+ {{#if errorCode}}
+ Error Code: {{errorCode}}
+ {{/if}}
diff --git a/README.md b/README.md index 488db88..5c05620 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,13 @@ GEMINI_API_KEY=your_gemini_api_key GOOGLE_APPLICATION_CREDENTIALS=path_to_google_credentials ``` +## Get Free API + +### Gemini +https://aistudio.google.com/app/apikey +### Nvidia(llama) + + ## Installation Guide 1. Clone the repository: diff --git a/index.js b/index.js index 32e7c8d..b31cec6 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ import path from "path"; import { fileURLToPath } from "url"; import dotenv from "dotenv"; import mainRoutes from "./routes/main.js"; +import dashboardRoutes from "./routes/dashboard.js"; import { engine } from "express-handlebars"; import Handlebars from "handlebars"; import minifyHTML from "express-minify-html"; @@ -36,28 +37,92 @@ router.use( ); // Configure Handlebars router.engine("handlebars", engine({ - defaultLayout: false, - partialsDir: [ - path.join(__dirname, "views/templates"), - path.join(__dirname, "views/notice"), - path.join(__dirname, "views") - ], - cache: false, - helpers: { // <-- ADD THIS helpers OBJECT - eq: function (a, b) { // <-- Move your helpers inside here - return a === b; - }, - encodeURIComponent: function (str) { - return encodeURIComponent(str); - }, - formatTimestamp: function (timestamp) { - return new Date(timestamp).toLocaleString(); - }, - jsonStringify: function (context) { - return JSON.stringify(context); - } + defaultLayout: false, + partialsDir: [ + path.join(__dirname, "views/templates"), + path.join(__dirname, "views/notice"), + path.join(__dirname, "views") + ], + cache: false, + helpers: { // <-- ADD THIS helpers OBJECT + eq: function (a, b) { // <-- Move your helpers inside here + return a === b; + }, + encodeURIComponent: function (str) { + return encodeURIComponent(str); + }, + formatTimestamp: function (timestamp) { + return new Date(timestamp).toLocaleString(); + }, + jsonStringify: function (context) { + return JSON.stringify(context); + }, + percentage: function (used, total) { + if (total === 0) return 0; + return Math.round((used / total) * 100); + }, formatDate: function (dateString) { + if (!dateString) return ''; + const date = new Date(dateString); + return date.toLocaleString(); + }, + gt: function (a, b) { + return a > b; + }, + lt: function (a, b) { + return a < b; + }, + add: function (a, b) { + return a + b; + }, + subtract: function (a, b) { + return a - b; + }, + range: function (start, end) { + const result = []; + for (let i = start; i <= end; i++) { + result.push(i); + } + return result; + }, + formatTime: function (index) { + // Simple implementation - in a real app you might want actual timestamps + return `Message ${index + 1}`; + }, + and: function (...args) { + // Remove the options object that is provided by Handlebars + const options = args.pop(); + return args.every(Boolean); + }, + neq: function (a, b, options) { + if (options && typeof options.fn === 'function') { + return a !== b ? options.fn(this) : options.inverse(this); + } + // Fallback for inline usage + return a !== b; + }, + truncate: function (str, len) { + if (typeof str !== "string") return ""; + // Default length = 50 if not provided + const limit = len || 50; + return str.length > limit ? str.substring(0, limit) + "..." : str; + }, + formatUptime: function () { // <-- New helper + const uptime = process.uptime(); + const h = Math.floor(uptime / 3600); + const m = Math.floor((uptime % 3600) / 60); + const s = Math.floor(uptime % 60); + return `${h}h ${m}m ${s}s`; } + } })); + +Handlebars.registerHelper('divide', function (value, divisor, multiplier) { + if (divisor == 0) { + return 0; + } + return (value / divisor) * multiplier; +}); + router.set("view engine", "handlebars"); router.set("views", path.join(__dirname, "views")); @@ -101,6 +166,11 @@ router.get("/info/Credits", async (req, res) => { router.use(mbkAuthRouter); router.use("/", mainRoutes); +router.use("/", dashboardRoutes); + +router.get("/admin/*", async (req, res) => { + res.redirect("/admin/dashboard"); +}); router.get('/simulate-error', (req, res, next) => { next(new Error('Simulated router error')); diff --git a/public/Assets/css/admin.css b/public/Assets/css/admin.css new file mode 100644 index 0000000..2b4bdd9 --- /dev/null +++ b/public/Assets/css/admin.css @@ -0,0 +1,1164 @@ +/* ===== Base Admin Styles ===== */ +.admin-container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +h1 { + color: #2c3e50; + border-bottom: 2px solid #3498db; + padding-bottom: 10px; + margin-bottom: 20px; +} + +h2 { + color: #2c3e50; + margin: 0; + font-size: 1.2rem; +} + +/* ===== Common Components ===== */ +.back-link { + color: #3498db; + text-decoration: none; + font-size: 0.9rem; +} + +.back-link:hover { + text-decoration: underline; +} + +.view-all-link { + color: #3498db; + text-decoration: none; + font-size: 0.9rem; +} + +.view-all-link:hover { + text-decoration: underline; +} + +.clear-filter { + color: #e74c3c; + text-decoration: none; + font-size: 0.9rem; + white-space: nowrap; +} + +.clear-filter:hover { + text-decoration: underline; +} + +.pagination { + display: flex; + justify-content: center; + gap: 5px; + margin-top: 20px; +} + +.page-link { + padding: 8px 12px; + background: #ecf0f1; + color: #2c3e50; + text-decoration: none; + border-radius: 4px; +} + +.page-link:hover, +.page-link.active { + background: #3498db; + color: white; +} + +/* ===== Filter/Search Forms ===== */ +.filter-form, +.search-form { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; +} + +.filter-group { + display: flex; + align-items: center; + gap: 5px; +} + +.filter-form input, +.filter-form select, +.search-form input, +.search-form select { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + min-width: 150px; +} + +.filter-form input[type="date"] { + padding: 7px 12px; +} + +.date-range-inputs span { + color: #7f8c8d; + font-size: 0.9rem; +} + +.filter-button, +.search-form button { + padding: 8px 15px; + background: #3498db; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.filter-button:hover, +.search-form button:hover { + background: #2980b9; +} + +/* ===== Action Bars ===== */ +.action-bar { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; + padding: 10px; + background: #f8f9fa; + border-radius: 4px; +} + +.selected-count { + font-size: 0.9rem; + color: #7f8c8d; +} + +.bulk-action-btn { + padding: 8px 15px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.9rem; +} + +.bulk-action-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +#bulkDeleteBtn { + background: #e74c3c; + color: white; +} + +#bulkDeleteBtn:not(:disabled):hover { + background: #c0392b; +} + +#exportSelectedBtn { + background: #27ae60; + color: white; +} + +#exportSelectedBtn:not(:disabled):hover { + background: #219653; +} + +.export-container { + display: flex; + align-items: center; + gap: 5px; +} + +.export-format { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + background: white; + font-size: 0.9rem; +} + +.export-btn { + display: flex; + align-items: center; + gap: 5px; +} + +.fa-download { + font-size: 0.9rem; +} + +/* ===== List Styles ===== */ +.chat-list, +.user-list { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + overflow: hidden; + margin-bottom: 20px; +} + +.chat-list-header, +.chat-row, +.user-list-header, +.user-row { + display: grid; + align-items: center; + padding: 12px 15px; +} + +.chat-list-header, +.user-list-header { + background: #2c3e50; + color: white; + font-weight: bold; +} + +.header-item { + padding: 5px; + overflow: hidden; + text-overflow: ellipsis; +} + +.sortable a { + color: white; + text-decoration: none; + display: flex; + align-items: center; + gap: 5px; +} + +.sorted a { + font-weight: bold; +} + +.checkbox-cell { + display: flex; + justify-content: center; +} + +.chat-row, +.user-row { + border-bottom: 1px solid #ecf0f1; +} + +.chat-row:last-child, +.user-row:last-child { + border-bottom: none; +} + +.chat-row:nth-child(even), +.user-row:nth-child(even) { + background-color: #f8f9fa; +} + +.chat-cell, +.user-cell { + padding: 8px 5px; + font-size: 0.9rem; + overflow: hidden; + text-overflow: ellipsis; +} + +.preview-cell { + white-space: nowrap; +} + +/* Chat List Specific */ +.chat-list-header, +.chat-row { + grid-template-columns: 50px 80px 1fr 1fr 1fr 80px 80px 2fr 100px; +} + +/* User List Specific */ +.user-list-header, +.user-row { + grid-template-columns: 1.5fr 1fr 0.8fr 0.8fr 0.8fr 1fr 1fr; +} + +/* ===== Action Buttons ===== */ +.view-btn, +.edit-btn, +.chats-link { + padding: 5px 10px; + border-radius: 4px; + font-size: 0.8rem; + text-decoration: none; +} + +.view-btn { + background: #3498db; + color: white; + display: inline-block; +} + +.view-btn:hover { + background: #2980b9; +} + +.edit-btn { + background: #3498db; + color: white; + margin-right: 5px; + border: none; + cursor: pointer; +} + +.edit-btn:hover { + background: #2980b9; +} + +.chats-link { + background: #27ae60; + color: white; + display: inline-block; +} + +.chats-link:hover { + background: #219653; +} + +/* ===== Dashboard Specific Styles ===== */ +.welcome-message { + color: #7f8c8d; + font-style: italic; + margin-bottom: 20px; + display: flex; + justify-content: space-between; +} + +.last-updated { + font-size: 0.8rem; + color: #95a5a6; +} + +.admin-stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; + margin-bottom: 30px; +} + +.stat-card { + background: #fff; + border-radius: 8px; + padding: 20px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + text-align: center; + position: relative; +} + +.stat-value { + font-size: 2.5rem; + font-weight: bold; + color: #3498db; + margin-bottom: 5px; +} + +.stat-label { + color: #7f8c8d; + font-size: 0.9rem; + margin-bottom: 10px; +} + +.stat-trend { + font-size: 0.8rem; + padding-top: 10px; + border-top: 1px solid #ecf0f1; +} + +.stat-trend.up { + color: #27ae60; +} + +.stat-trend.down { + color: #e74c3c; +} + +.admin-section { + margin-bottom: 30px; + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + padding: 20px; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.recent-chats { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 15px; +} + +.chat-item { + background: #f8f9fa; + border-radius: 8px; + padding: 15px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.chat-header { + display: flex; + justify-content: space-between; + margin-bottom: 10px; +} + +.chat-user { + font-weight: bold; + color: #2c3e50; +} + +.chat-time { + color: #7f8c8d; + font-size: 0.8rem; +} + +.chat-meta { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 5px; + font-size: 0.8rem; + color: #7f8c8d; + margin-bottom: 10px; +} + +.chat-meta span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-usage { + margin-bottom: 10px; + position: relative; + height: 20px; + background: #ecf0f1; + border-radius: 4px; + overflow: hidden; +} + +.usage-bar { + height: 100%; + background: #3498db; + transition: width 0.3s; +} + +.chat-usage span { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 0.7rem; + color: white; + text-shadow: 0 0 2px rgba(0,0,0,0.5); +} + +.admin-columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px; + margin-bottom: 30px; +} + +.admin-column { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + padding: 20px; +} + +.top-users, +.model-distribution { + display: flex; + flex-direction: column; + gap: 10px; +} + +.user-item, +.model-item { + display: flex; + justify-content: space-between; + padding: 10px; + border-radius: 4px; + background: #f8f9fa; +} + +.user-info, +.model-info { + display: flex; + flex-direction: column; +} + +.user-name, +.model-name { + font-weight: bold; + margin-bottom: 3px; +} + +.user-stats, +.model-stats { + font-size: 0.7rem; + color: #7f8c8d; +} + +.user-messages, +.model-usage { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.message-count, +.model-count { + font-weight: bold; +} + +.peak-messages, +.model-limit { + font-size: 0.7rem; + color: #7f8c8d; +} + +.quick-actions { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + padding: 20px; +} + +.action-buttons { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 15px; + margin-top: 15px; +} + +.action-button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; + background: #3498db; + color: white; + border-radius: 8px; + text-decoration: none; + transition: transform 0.2s, box-shadow 0.2s; +} + +.action-button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); +} + +.action-button i { + font-size: 1.5rem; + margin-bottom: 10px; +} + +.action-button span { + font-size: 0.9rem; +} + +/* ===== Error Page Styles ===== */ +.error-message { + background: #fff; + border-radius: 8px; + padding: 20px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + margin-top: 20px; + text-align: center; +} + +.error-message p { + font-size: 1.1rem; + margin-bottom: 20px; + color: #2c3e50; +} + +/* ===== Modal Styles ===== */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); +} + +.modal-content { + background: white; + margin: 10% auto; + padding: 25px; + border-radius: 8px; + width: 450px; + max-width: 90%; + position: relative; + box-shadow: 0 4px 20px rgba(0,0,0,0.2); +} + +.close-modal { + position: absolute; + right: 20px; + top: 15px; + font-size: 1.5rem; + cursor: pointer; + color: #7f8c8d; +} + +.close-modal:hover { + color: #2c3e50; +} + +.modal h2 { + margin-top: 0; + border-bottom: none; + padding-bottom: 0; + color: #2c3e50; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: bold; + color: #2c3e50; +} + +.form-control { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +.form-control:focus { + border-color: #3498db; + outline: none; + box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 25px; +} + +.submit-btn { + background: #27ae60; + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; +} + +.submit-btn:hover { + background: #219653; +} + +.cancel-btn { + background: #95a5a6; + color: white; + border: none; + padding: 10px 20px; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; +} + +.cancel-btn:hover { + background: #7f8c8d; +} + +/* ===== Animations ===== */ +@keyframes fa-spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(359deg); + } +} + + + + + + + + + + + + + + + + + + + + + +/* ===== Base Admin Styles ===== */ +:root { + --primary-color: #4361ee; + --primary-hover: #3a56d4; + --secondary-color: #3f37c9; + --success-color: #4cc9f0; + --danger-color: #f72585; + --warning-color: #f8961e; + --info-color: #4895ef; + --light-color: #f8f9fa; + --dark-color: #212529; + --text-color: #2b2d42; + --text-light: #8d99ae; + --border-color: #e9ecef; + --bg-color: #f8f9fa; + --card-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + --hover-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); + --transition: all 0.3s ease; +} + +.admin-container { + max-width: 1600px; + margin: 0 auto; + padding: 2rem; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + color: var(--text-color); + background-color: var(--bg-color); + min-height: 100vh; +} + +h1 { + color: var(--dark-color); + font-weight: 700; + font-size: 2rem; + margin-bottom: 1.5rem; + position: relative; + padding-bottom: 0.75rem; +} + +h1::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 60px; + height: 4px; + background: var(--primary-color); + border-radius: 2px; +} + +h2 { + color: var(--dark-color); + font-weight: 600; + font-size: 1.5rem; + margin: 0; +} + +/* ===== Common Components ===== */ +.link { + color: var(--primary-color); + text-decoration: none; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 0.25rem; + transition: var(--transition); +} + +.link:hover { + color: var(--primary-hover); + text-decoration: underline; +} + +.link i { + font-size: 0.9em; +} + +.btn { + padding: 0.5rem 1rem; + border-radius: 6px; + font-weight: 500; + cursor: pointer; + transition: var(--transition); + display: inline-flex; + align-items: center; + gap: 0.5rem; + border: none; +} + +.btn-primary { + background-color: var(--primary-color); + color: white; +} + +.btn-primary:hover { + background-color: var(--primary-hover); + box-shadow: 0 2px 8px rgba(67, 97, 238, 0.3); +} + +.btn-danger { + background-color: var(--danger-color); + color: white; +} + +.btn-danger:hover { + background-color: #e5177e; + box-shadow: 0 2px 8px rgba(247, 37, 133, 0.3); +} + +.btn-success { + background-color: var(--success-color); + color: white; +} + +.btn-success:hover { + background-color: #3ab5d8; + box-shadow: 0 2px 8px rgba(76, 201, 240, 0.3); +} + +.btn-sm { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; +} + +/* ===== Cards ===== */ +.card { + background: white; + border-radius: 12px; + box-shadow: var(--card-shadow); + transition: var(--transition); + overflow: hidden; +} + +.card:hover { + box-shadow: var(--hover-shadow); +} + +.card-header { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +.card-body { + padding: 1.5rem; +} + +/* ===== Tables ===== */ +.table-responsive { + overflow-x: auto; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th { + background-color: var(--primary-color); + color: white; + font-weight: 600; + padding: 1rem; + text-align: left; +} + +.table td { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-color); +} + +.table tr:last-child td { + border-bottom: none; +} + +.table tr:nth-child(even) { + background-color: rgba(248, 249, 250, 0.5); +} + +.table tr:hover { + background-color: rgba(67, 97, 238, 0.05); +} + +/* ===== Forms ===== */ +.form-control { + width: 100%; + padding: 0.625rem 0.875rem; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 1rem; + transition: var(--transition); +} + +.form-control:focus { + border-color: var(--primary-color); + outline: none; + box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2); +} + +.form-group { + margin-bottom: 1.25rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; +} + +/* ===== Dashboard Specific ===== */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: var(--card-shadow); + position: relative; + overflow: hidden; +} + +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background: var(--primary-color); +} + +.stat-value { + font-size: 2.25rem; + font-weight: 700; + color: var(--dark-color); + margin-bottom: 0.25rem; +} + +.stat-label { + color: var(--text-light); + font-size: 0.875rem; + margin-bottom: 0.5rem; +} + +.stat-trend { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.875rem; + padding-top: 0.75rem; + border-top: 1px solid var(--border-color); +} + +.trend-up { + color: #4cc9f0; +} + +.trend-down { + color: #f72585; +} + +/* ===== Layout ===== */ +.grid-cols-2 { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; +} + +@media (max-width: 1024px) { + .grid-cols-2 { + grid-template-columns: 1fr; + } +} + +/* ===== Recent Chats ===== */ +.recent-chats { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.chat-card { + background: white; + border-radius: 8px; + padding: 1rem; + box-shadow: var(--card-shadow); + transition: var(--transition); +} + +.chat-card:hover { + transform: translateY(-2px); + box-shadow: var(--hover-shadow); +} + +.chat-header { + display: flex; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.chat-user { + font-weight: 600; + color: var(--dark-color); +} + +.chat-time { + color: var(--text-light); + font-size: 0.75rem; +} + +.chat-meta { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; + font-size: 0.75rem; + color: var(--text-light); + margin-bottom: 0.75rem; +} + +.progress-bar { + height: 8px; + background-color: #e9ecef; + border-radius: 4px; + overflow: hidden; + margin-bottom: 0.75rem; +} + +.progress-value { + height: 100%; + background-color: var(--primary-color); + transition: width 0.3s ease; +} + +/* ===== Quick Actions ===== */ +.quick-actions { + margin-top: 2rem; +} + +.action-buttons { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.action-button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1.5rem; + background: white; + color: var(--primary-color); + border-radius: 8px; + text-decoration: none; + box-shadow: var(--card-shadow); + transition: var(--transition); +} + +.action-button:hover { + transform: translateY(-3px); + box-shadow: var(--hover-shadow); + color: var(--primary-hover); +} + +.action-button i { + font-size: 1.75rem; + margin-bottom: 0.75rem; +} + +.action-button span { + font-size: 0.875rem; + font-weight: 500; +} + +/* ===== Pagination ===== */ +.pagination { + display: flex; + justify-content: center; + gap: 0.5rem; + margin-top: 2rem; +} + +.page-link { + padding: 0.5rem 0.75rem; + background: white; + color: var(--text-color); + text-decoration: none; + border-radius: 6px; + box-shadow: var(--card-shadow); + transition: var(--transition); +} + +.page-link:hover, +.page-link.active { + background: var(--primary-color); + color: white; +} + +/* ===== Animations ===== */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.fade-in { + animation: fadeIn 0.3s ease-out forwards; +} + +/* ===== Responsive Adjustments ===== */ +@media (max-width: 768px) { + .admin-container { + padding: 1rem; + } + + .stats-grid { + grid-template-columns: 1fr 1fr; + } + + .recent-chats { + grid-template-columns: 1fr; + } +} + +@media (max-width: 576px) { + .stats-grid { + grid-template-columns: 1fr; + } + + h1 { + font-size: 1.5rem; + } + + h2 { + font-size: 1.25rem; + } +} \ No newline at end of file diff --git a/routes/dashboard.js b/routes/dashboard.js new file mode 100644 index 0000000..8f7bc85 --- /dev/null +++ b/routes/dashboard.js @@ -0,0 +1,571 @@ +import express from "express"; +import dotenv from "dotenv"; +import fetch from 'node-fetch'; +import { pool } from "./pool.js"; +import { validateSession, validateSessionAndRole } from "mbkauthe"; +import { checkMessageLimit } from "./checkMessageLimit.js"; + +dotenv.config(); +const router = express.Router(); + + +// Constants +const DEFAULT_PAGE_SIZE_s = 20; +const DATE_RANGE_OPTIONS = [ + { value: 'today', label: 'Today' }, + { value: 'yesterday', label: 'Yesterday' }, + { value: 'last7', label: 'Last 7 Days' }, + { value: 'last30', label: 'Last 30 Days' }, + { value: 'custom', label: 'Custom Range' } +]; + +// Admin dashboard route with more statistics +router.get("/admin/dashboard", validateSessionAndRole("SuperAdmin"), async (req, res) => { + try { + // Get statistics + const statsQuery = ` + SELECT + (SELECT COUNT(*) FROM ai_history_chatapi) as total_chats, + (SELECT COUNT(DISTINCT username) FROM ai_history_chatapi) as unique_users, + (SELECT COUNT(*) FROM user_settings_chatapi) as user_settings_count, + (SELECT COUNT(*) FROM user_message_logs_chatapi WHERE date = CURRENT_DATE) as active_users_today, + (SELECT SUM(message_count) FROM user_message_logs_chatapi WHERE date = CURRENT_DATE) as messages_today, + (SELECT COUNT(*) FROM ai_history_chatapi WHERE created_at >= NOW() - INTERVAL '1 hour') as active_chats_last_hour + `; + + // Get recent chats with more details + const recentChatsQuery = ` + SELECT + a.id, + a.username, + a.created_at, + a.temperature, + u.daily_message_limit, + l.message_count, + u.ai_model, + jsonb_array_length(a.conversation_history) as message_count_in_chat + FROM ai_history_chatapi a + LEFT JOIN user_settings_chatapi u ON a.username = u.username + LEFT JOIN user_message_logs_chatapi l ON a.username = l.username AND l.date = CURRENT_DATE + ORDER BY a.created_at DESC + LIMIT 10 + `; + + // Get top users with more metrics + const topUsersQuery = ` + SELECT + username, + SUM(message_count) as total_messages, + COUNT(DISTINCT date) as active_days, + MAX(message_count) as peak_messages_in_day + FROM user_message_logs_chatapi + GROUP BY username + ORDER BY total_messages DESC + LIMIT 5 + `; + + // Get model distribution with average temperature + // In the admin dashboard route, update the modelDistributionQuery to cast AVG values to numeric + const modelDistributionQuery = ` + SELECT + ai_model, + COUNT(*) as count, + ROUND(AVG(temperature)::numeric, 2) as avg_temp, + ROUND(AVG(daily_message_limit)::numeric, 2) as avg_limit + FROM user_settings_chatapi + GROUP BY ai_model + ORDER BY count DESC +`; + + // Get hourly message volume for today + const hourlyVolumeQuery = ` + SELECT + EXTRACT(HOUR FROM created_at) as hour, + COUNT(*) as message_count + FROM ai_history_chatapi + WHERE created_at >= CURRENT_DATE + GROUP BY hour + ORDER BY hour + `; + + const [ + statsResult, + recentChatsResult, + topUsersResult, + modelDistributionResult, + hourlyVolumeResult + ] = await Promise.all([ + pool.query(statsQuery), + pool.query(recentChatsQuery), + pool.query(topUsersQuery), + pool.query(modelDistributionQuery), + pool.query(hourlyVolumeQuery) + ]); + + const stats = statsResult.rows[0]; + const recentChats = recentChatsResult.rows; + const topUsers = topUsersResult.rows; + const modelDistribution = modelDistributionResult.rows; + const hourlyVolume = hourlyVolumeResult.rows; + + // Format hourly data for chart + const hourlyData = Array(24).fill(0); + hourlyVolume.forEach(row => { + hourlyData[parseInt(row.hour)] = parseInt(row.message_count); + }); + + res.render("admin/dashboard.handlebars", { + stats, + recentChats, + topUsers, + modelDistribution, + hourlyData: JSON.stringify(hourlyData), + currentUser: req.session.user.username, + dateRangeOptions: DATE_RANGE_OPTIONS + }); + } catch (error) { + console.error("Error in admin dashboard:", error); + res.status(500).render("admin/error.handlebars", { + message: "Failed to load dashboard data", + error: error.message + }); + } +}); + +// Enhanced User management route with filtering +router.get("/admin/users", validateSessionAndRole("SuperAdmin"), async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const pageSize = parseInt(req.query.pageSize) || DEFAULT_PAGE_SIZE_s; + const offset = (page - 1) * pageSize; + const { search, model, sort, order } = req.query; + + let baseQuery = ` + FROM user_settings_chatapi u + LEFT JOIN ( + SELECT username, COUNT(*) as chat_count + FROM ai_history_chatapi + GROUP BY username + ) a ON u.username = a.username + LEFT JOIN ( + SELECT username, SUM(message_count) as total_messages + FROM user_message_logs_chatapi + GROUP BY username + ) l ON u.username = l.username + `; + + let whereClause = ""; + const queryParams = []; + + // Add filters + if (search) { + whereClause += ` WHERE u.username ILIKE $${queryParams.length + 1}`; + queryParams.push(`%${search}%`); + } + + if (model) { + whereClause += whereClause ? ' AND' : ' WHERE'; + whereClause += ` u.ai_model = $${queryParams.length + 1}`; + queryParams.push(model); + } + + // Get total count + const countQuery = `SELECT COUNT(*) ${baseQuery} ${whereClause}`; + const countResult = await pool.query(countQuery, queryParams); + const totalCount = parseInt(countResult.rows[0].count); + + // Determine sort + const sortField = sort || 'u.created_at'; + const sortOrder = order === 'asc' ? 'ASC' : 'DESC'; + + // Get paginated users with more metrics + const usersQuery = ` + SELECT + u.username, + u.ai_model, + u.daily_message_limit, + u.created_at as settings_created, + u.temperature, + COALESCE(a.chat_count, 0) as chat_count, + COALESCE(l.total_messages, 0) as total_messages, + (SELECT COUNT(*) FROM user_message_logs_chatapi WHERE username = u.username) as active_days + ${baseQuery} + ${whereClause} + ORDER BY ${sortField} ${sortOrder} + LIMIT $${queryParams.length + 1} OFFSET $${queryParams.length + 2} + `; + + const usersResult = await pool.query(usersQuery, [...queryParams, pageSize, offset]); + const users = usersResult.rows; + + // Get available models for filter dropdown + const modelsResult = await pool.query(` + SELECT DISTINCT ai_model FROM user_settings_chatapi ORDER BY ai_model + `); + const availableModels = modelsResult.rows.map(row => row.ai_model); + + res.render("admin/users.handlebars", { + users, + availableModels, + searchQuery: search || "", + selectedModel: model || "", + sortField, + sortOrder, + pagination: { + currentPage: page, + pageSize, + totalCount, + totalPages: Math.ceil(totalCount / pageSize) + }, + currentUser: req.session.user.username + }); + } catch (error) { + console.error("Error in user management:", error); + res.status(500).render("admin/error.handlebars", { + message: "Failed to load user data", + error: error.message + }); + } +}); + +// Enhanced Chat management route with date filtering +router.get("/admin/chats", validateSessionAndRole("SuperAdmin"), async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const pageSize = parseInt(req.query.pageSize) || DEFAULT_PAGE_SIZE_s; + const offset = (page - 1) * pageSize; + const { username, model, dateRange, startDate, endDate, search } = req.query; + + let baseQuery = ` + FROM ai_history_chatapi a + LEFT JOIN user_settings_chatapi u ON a.username = u.username + `; + + let whereClause = ""; + const queryParams = []; + + // Add username filter + if (username) { + whereClause += " WHERE a.username = $1"; + queryParams.push(username); + } + + // Add model filter + if (model) { + whereClause += whereClause ? ' AND' : ' WHERE'; + whereClause += ` u.ai_model = $${queryParams.length + 1}`; + queryParams.push(model); + } + + // Add date range filter + if (dateRange) { + let dateCondition = ""; + switch (dateRange) { + case 'today': + dateCondition = "a.created_at >= CURRENT_DATE"; + break; + case 'yesterday': + dateCondition = "a.created_at >= CURRENT_DATE - INTERVAL '1 day' AND a.created_at < CURRENT_DATE"; + break; + case 'last7': + dateCondition = "a.created_at >= CURRENT_DATE - INTERVAL '7 days'"; + break; + case 'last30': + dateCondition = "a.created_at >= CURRENT_DATE - INTERVAL '30 days'"; + break; + case 'custom': + if (startDate && endDate) { + dateCondition = `a.created_at >= $${queryParams.length + 1} AND a.created_at <= $${queryParams.length + 2}`; + queryParams.push(startDate, endDate + ' 23:59:59'); + } + break; + } + + if (dateCondition) { + whereClause += whereClause ? ' AND' : ' WHERE'; + whereClause += ` ${dateCondition}`; + } + } + + // Add search filter (searches within conversation history) + if (search) { + whereClause += whereClause ? ' AND' : ' WHERE'; + whereClause += ` a.conversation_history::text ILIKE $${queryParams.length + 1}`; + queryParams.push(`%${search}%`); + } + + // Get total count + const countQuery = `SELECT COUNT(*) ${baseQuery} ${whereClause}`; + const countResult = await pool.query(countQuery, queryParams); + const totalCount = parseInt(countResult.rows[0].count); + + // Get paginated chats with more details + const chatsQuery = ` + SELECT + a.id, + a.username, + a.created_at, + a.temperature, + u.ai_model, + jsonb_array_length(a.conversation_history) as message_count, + (a.conversation_history->0->'parts'->0->>'text') as first_message_preview + ${baseQuery} + ${whereClause} + ORDER BY a.created_at DESC + LIMIT $${queryParams.length + 1} OFFSET $${queryParams.length + 2} + `; + + const chatsResult = await pool.query( + chatsQuery, + [...queryParams, pageSize, offset] + ); + + const chats = chatsResult.rows; + + // Get available models for filter dropdown + const modelsResult = await pool.query(` + SELECT DISTINCT ai_model FROM user_settings_chatapi ORDER BY ai_model + `); + const availableModels = modelsResult.rows.map(row => row.ai_model); + + res.render("admin/chats.handlebars", { + chats, + usernameFilter: username || "", + selectedModel: model || "", + dateRangeOptions: DATE_RANGE_OPTIONS, + selectedDateRange: dateRange || "", + startDate: startDate || "", + endDate: endDate || "", + searchQuery: search || "", + availableModels, + pagination: { + currentPage: page, + pageSize, + totalCount, + totalPages: Math.ceil(totalCount / pageSize) + }, + currentUser: req.session.user.username + }); + } catch (error) { + console.error("Error in chat management:", error); + res.status(500).render("admin/error.handlebars", { + message: "Failed to load chat data", + error: error.message + }); + } +}); + +// Bulk operations for chats +router.post("/admin/chats/bulk-delete", validateSessionAndRole("SuperAdmin"), async (req, res) => { + try { + const { chatIds } = req.body; + + if (!chatIds || !Array.isArray(chatIds) || chatIds.length === 0) { + return res.status(400).json({ success: false, message: "No chat IDs provided" }); + } + + await pool.query(` + DELETE FROM ai_history_chatapi + WHERE id = ANY($1::int[]) + `, [chatIds]); + + res.json({ + success: true, + message: `${chatIds.length} chats deleted successfully` + }); + } catch (error) { + console.error("Error in bulk delete:", error); + res.status(500).json({ + success: false, + message: "Failed to delete chats", + error: error.message + }); + } +}); + +// User export functionality +router.get("/admin/users/export", validateSessionAndRole("SuperAdmin"), async (req, res) => { + try { + const { format = 'json' } = req.query; + + const usersQuery = ` + SELECT + u.username, + u.ai_model, + u.daily_message_limit, + u.created_at as settings_created, + u.temperature, + COALESCE(COUNT(a.id), 0) as chat_count, + COALESCE(SUM(l.message_count), 0) as total_messages + FROM user_settings_chatapi u + LEFT JOIN ai_history_chatapi a ON u.username = a.username + LEFT JOIN user_message_logs_chatapi l ON u.username = l.username + GROUP BY u.username, u.ai_model, u.daily_message_limit, u.created_at, u.temperature + ORDER BY u.created_at DESC + `; + + const usersResult = await pool.query(usersQuery); + const users = usersResult.rows; + + if (format === 'csv') { + const fields = ['username', 'ai_model', 'daily_message_limit', 'settings_created', 'temperature', 'chat_count', 'total_messages']; + const csv = [ + fields.join(','), // header + ...users.map(user => fields.map(field => { + const value = user[field]; + return typeof value === 'string' ? `"${value.replace(/"/g, '""')}"` : value; + }).join(',')) + ].join('\n'); + + res.header('Content-Type', 'text/csv'); + res.header('Content-Disposition', 'attachment; filename=users_export.csv'); + return res.send(csv); + } + + // Default to JSON + res.json(users); + } catch (error) { + console.error("Error exporting users:", error); + res.status(500).json({ + success: false, + message: "Failed to export user data", + error: error.message + }); + } +}); + +// Update system settings +router.post("/admin/settings/update", validateSessionAndRole("SuperAdmin"), async (req, res) => { + try { + const { updates } = req.body; + + if (!updates || typeof updates !== 'object') { + return res.status(400).json({ success: false, message: "Invalid updates" }); + } + + // In a real application, you would save these to a configuration store + // For this example, we'll just return them + res.json({ + success: true, + message: "Settings updated (simulated)", + updates + }); + } catch (error) { + console.error("Error updating settings:", error); + res.status(500).json({ + success: false, + message: "Failed to update settings", + error: error.message + }); + } +}); + +// Add this near your other routes in adminRoutes.js + +// Export chats route +router.post("/admin/chats/export", validateSessionAndRole("SuperAdmin"), async (req, res) => { + try { + const { chatIds, format = 'json' } = req.body; + + if (!chatIds || !Array.isArray(chatIds) || chatIds.length === 0) { + return res.status(400).json({ success: false, message: "No chat IDs provided" }); + } + + // Get the chat data + const chatsQuery = ` + SELECT + a.id, + a.username, + a.conversation_history, + a.created_at, + a.temperature, + u.ai_model + FROM ai_history_chatapi a + LEFT JOIN user_settings_chatapi u ON a.username = u.username + WHERE a.id = ANY($1::int[]) + ORDER BY a.created_at DESC + `; + + const chatsResult = await pool.query(chatsQuery, [chatIds]); + const chats = chatsResult.rows; + + if (format === 'csv') { + // Generate CSV + const fields = ['id', 'username', 'created_at', 'temperature', 'ai_model', 'message_count', 'conversation']; + const csvRows = chats.map(chat => { + const conversationText = typeof chat.conversation_history === 'string' + ? JSON.parse(chat.conversation_history) + .map(msg => `${msg.role}: ${msg.parts?.[0]?.text || ''}`) + .join('\n') + : chat.conversation_history + .map(msg => `${msg.role}: ${msg.parts?.[0]?.text || ''}`) + .join('\n'); + + return { + id: chat.id, + username: chat.username, + created_at: chat.created_at, + temperature: chat.temperature, + ai_model: chat.ai_model, + message_count: Array.isArray(chat.conversation_history) + ? chat.conversation_history.length + : JSON.parse(chat.conversation_history).length, + conversation: conversationText + }; + }); + + // Convert to CSV string + const csv = [ + fields.join(','), // header + ...csvRows.map(row => fields.map(field => { + const value = row[field]; + return typeof value === 'string' ? `"${value.replace(/"/g, '""')}"` : value; + }).join(',')) + ].join('\n'); + + res.header('Content-Type', 'text/csv'); + res.header('Content-Disposition', 'attachment; filename=chats_export.csv'); + return res.send(csv); + } + + // Default to JSON + const formattedChats = chats.map(chat => ({ + id: chat.id, + username: chat.username, + created_at: chat.created_at, + temperature: chat.temperature, + ai_model: chat.ai_model, + conversation_history: typeof chat.conversation_history === 'string' + ? JSON.parse(chat.conversation_history) + : chat.conversation_history + })); + + // In your /admin/chats/export route + if (format === 'json' || format === 'json-raw') { + const jsonData = JSON.stringify(formattedChats, null, 2); + + if (format === 'json') { + res.header('Content-Type', 'application/json'); + res.header('Content-Disposition', 'attachment; filename=chats_export.json'); + return res.send(jsonData); + } else { + // json-raw format + return res.json(formattedChats); + } + } else { + // Return as downloadable file + res.header('Content-Type', 'application/json'); + res.header('Content-Disposition', 'attachment; filename=chats_export.json'); + res.send(JSON.stringify(formattedChats, null, 2)); + } + } catch (error) { + console.error("Error exporting chats:", error); + res.status(500).json({ + success: false, + message: "Failed to export chats", + error: error.message + }); + } +}); + +export default router; \ No newline at end of file diff --git a/routes/main.js b/routes/main.js index b5880af..f4ef680 100644 --- a/routes/main.js +++ b/routes/main.js @@ -4,54 +4,47 @@ import fetch from 'node-fetch'; import { pool } from "./pool.js"; import { validateSession, validateSessionAndRole } from "mbkauthe"; import { checkMessageLimit } from "./checkMessageLimit.js"; -import { GoogleAuth } from 'google-auth-library'; -import { google } from 'googleapis'; dotenv.config(); const router = express.Router(); -// Middleware -router.use(express.json()); -router.use(express.urlencoded({ extended: true })); - // Constants const DEFAULT_MODEL = 'gemini/gemini-1.5-flash'; const DEFAULT_TEMPERATURE = 1.0; const DEFAULT_THEME = 'dark'; const DEFAULT_FONT_SIZE = 16; const DEFAULT_DAILY_LIMIT = 100; - const DEFAULT_PAGE_SIZE = 20; +const IDENTITY_QUESTIONS = [ + /who\s*(are|is)\s*you/i, + /what\s*(are|is)\s*you/i, + /your\s*(name|identity)/i, + /introduce\s*yourself/i, + /are\s*you\s*(chatgpt|gemini|ai|bot)/i +]; -// API Clients -let googleAuthError = null; -let serviceusage = null; -let projectId = null; - -// Initialize Google Auth -(async () => { - try { - const auth = new GoogleAuth({ - scopes: ['https://www.googleapis.com/auth/cloud-platform'], - keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS - }); - const authClient = await auth.getClient(); - projectId = await auth.getProjectId(); - serviceusage = google.serviceusage('v1'); - } catch (error) { - googleAuthError = error; - } -})(); +// Middleware +router.use(express.json()); +router.use(express.urlencoded({ extended: true })); // Utility Functions const formatChatTime = (createdAt) => { const now = new Date(); const diffInSeconds = Math.floor((now - createdAt) / 1000); - if (diffInSeconds < 60) return `${diffInSeconds} second${diffInSeconds > 1 ? 's' : ''} ago`; - if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minute${Math.floor(diffInSeconds / 60) > 1 ? 's' : ''} ago`; - if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hour${Math.floor(diffInSeconds / 3600) > 1 ? 's' : ''} ago`; - if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)} day${Math.floor(diffInSeconds / 86400) > 1 ? 's' : ''} ago`; + const intervals = [ + { seconds: 60, text: 'second' }, + { seconds: 3600, text: 'minute', divisor: 60 }, + { seconds: 86400, text: 'hour', divisor: 3600 }, + { seconds: 2592000, text: 'day', divisor: 86400 } + ]; + + for (const interval of intervals) { + if (diffInSeconds < interval.seconds) { + const value = Math.floor(diffInSeconds / (interval.divisor || 1)); + return `${value} ${interval.text}${value > 1 ? 's' : ''} ago`; + } + } return createdAt.toLocaleDateString(); }; @@ -65,31 +58,32 @@ const handleApiError = (res, error, context) => { }); }; +const isIdentityQuestion = (text) => + IDENTITY_QUESTIONS.some(regex => regex.test(text)); + // Database Operations const db = { - fetchChatHistories: async (username, page = 1, pageSize = DEFAULT_PAGE_SIZE) => { + const offset = (page - 1) * pageSize; + const monthYearFormat = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long' }); + try { - const offset = (page - 1) * pageSize; + const [countResult, chatResult] = await Promise.all([ + pool.query( + 'SELECT COUNT(*) FROM ai_history_chatapi WHERE username = $1', + [username] + ), + pool.query( + `SELECT id, created_at, temperature + FROM ai_history_chatapi + WHERE username = $1 + ORDER BY created_at DESC + LIMIT $2 OFFSET $3`, + [username, pageSize, offset] + ) + ]); - // Get total count - const countResult = await pool.query( - 'SELECT COUNT(*) FROM ai_history_chatapi WHERE username = $1', - [username] - ); const totalCount = parseInt(countResult.rows[0].count); - - // Get paginated results - const { rows } = await pool.query( - `SELECT id, created_at, temperature - FROM ai_history_chatapi - WHERE username = $1 - ORDER BY created_at DESC - LIMIT $2 OFFSET $3`, - [username, pageSize, offset] - ); - - // Group chats by time period (your existing logic) const now = new Date(); const today = new Date(now.setHours(0, 0, 0, 0)); const yesterday = new Date(today); @@ -104,7 +98,7 @@ const db = { yesterday: [], last7Days: [], last30Days: [], - older: [], + older: {}, pagination: { currentPage: page, pageSize, @@ -113,50 +107,28 @@ const db = { } }; - // Your existing grouping logic... - rows.forEach(row => { + chatResult.rows.forEach(row => { const createdAt = new Date(row.created_at); const formattedDate = formatChatTime(createdAt); - // Add this line near the top of the file, after imports and constants - const monthYearFormat = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long' }); + const chatItem = { + id: row.id, + created_at: formattedDate, + temperature: row.temperature || 0.5, + rawDate: createdAt + }; + if (createdAt >= today) { - groupedChats.today.push({ - id: row.id, - created_at: formattedDate, - temperature: row.temperature || 0.5, - rawDate: createdAt - }); + groupedChats.today.push(chatItem); } else if (createdAt >= yesterday) { - groupedChats.yesterday.push({ - id: row.id, - created_at: formattedDate, - temperature: row.temperature || 0.5, - rawDate: createdAt - }); + groupedChats.yesterday.push(chatItem); } else if (createdAt >= sevenDaysAgo) { - groupedChats.last7Days.push({ - id: row.id, - created_at: formattedDate, - temperature: row.temperature || 0.5, - rawDate: createdAt - }); + groupedChats.last7Days.push(chatItem); } else if (createdAt >= thirtyDaysAgo) { - groupedChats.last30Days.push({ - id: row.id, - created_at: formattedDate, - temperature: row.temperature || 0.5, - rawDate: createdAt - }); + groupedChats.last30Days.push(chatItem); } else { - if (!groupedChats.older[monthYear]) { - groupedChats.older[monthYear] = []; - } - groupedChats.older[monthYear].push({ - id: row.id, - created_at: formattedDate, - temperature: row.temperature || 0.5, - rawDate: createdAt - }); + const monthYear = monthYearFormat.format(createdAt); + groupedChats.older[monthYear] = groupedChats.older[monthYear] || []; + groupedChats.older[monthYear].push(chatItem); } }); @@ -168,7 +140,7 @@ const db = { yesterday: [], last7Days: [], last30Days: [], - older: [], + older: {}, pagination: { currentPage: 1, pageSize: DEFAULT_PAGE_SIZE, @@ -182,7 +154,7 @@ const db = { fetchChatHistoryById: async (chatId) => { try { const { rows } = await pool.query( - 'SELECT id, conversation_history, temperature FROM ai_history_chatapi WHERE id = $1', + 'SELECT id, conversation_history, username, temperature FROM ai_history_chatapi WHERE id = $1', [chatId] ); return rows[0] || null; @@ -200,13 +172,13 @@ const db = { [JSON.stringify(history), chatId, temperature] ); return chatId; - } else { - const { rows } = await pool.query( - 'INSERT INTO ai_history_chatapi (conversation_history, username, temperature) VALUES ($1, $2, $3) RETURNING id', - [JSON.stringify(history), username, temperature] - ); - return rows[0].id; } + + const { rows } = await pool.query( + 'INSERT INTO ai_history_chatapi (conversation_history, username, temperature) VALUES ($1, $2, $3) RETURNING id', + [JSON.stringify(history), username, temperature] + ); + return rows[0].id; } catch (error) { console.error("Database error saving chat history:", error); throw error; @@ -214,8 +186,17 @@ const db = { }, fetchUserSettings: async (username) => { + const today = new Date().toISOString().split("T")[0]; + const defaultSettings = { + theme: DEFAULT_THEME, + font_size: DEFAULT_FONT_SIZE, + ai_model: DEFAULT_MODEL, + temperature: DEFAULT_TEMPERATURE, + dailyLimit: DEFAULT_DAILY_LIMIT, + messageCount: 0 + }; + try { - const today = new Date().toISOString().split("T")[0]; const [settingsResult, messageLog] = await Promise.all([ pool.query( 'SELECT theme, font_size, ai_model, temperature, daily_message_limit FROM user_settings_chatapi WHERE username = $1', @@ -227,33 +208,18 @@ const db = { ) ]); - const messageCount = messageLog.rows[0]?.message_count || 0; - - return settingsResult.rows.length > 0 ? { - theme: settingsResult.rows[0].theme || DEFAULT_THEME, - font_size: settingsResult.rows[0].font_size || DEFAULT_FONT_SIZE, - ai_model: settingsResult.rows[0].ai_model || DEFAULT_MODEL, - temperature: settingsResult.rows[0].temperature || DEFAULT_TEMPERATURE, - dailyLimit: settingsResult.rows[0].daily_message_limit || DEFAULT_DAILY_LIMIT, - messageCount - } : { - theme: DEFAULT_THEME, - font_size: DEFAULT_FONT_SIZE, - ai_model: DEFAULT_MODEL, - temperature: DEFAULT_TEMPERATURE, - dailyLimit: DEFAULT_DAILY_LIMIT, - messageCount: 0 + if (settingsResult.rows.length === 0) { + return defaultSettings; + } + + return { + ...defaultSettings, + ...settingsResult.rows[0], + messageCount: messageLog.rows[0]?.message_count || 0 }; } catch (error) { console.error("Database error fetching user settings:", error); - return { - theme: DEFAULT_THEME, - font_size: DEFAULT_FONT_SIZE, - ai_model: DEFAULT_MODEL, - temperature: DEFAULT_TEMPERATURE, - dailyLimit: DEFAULT_DAILY_LIMIT, - messageCount: 0 - }; + return defaultSettings; } }, @@ -264,10 +230,10 @@ const db = { VALUES ($1, $2, $3, $4, $5) ON CONFLICT (username) DO UPDATE SET - theme = $2, - font_size = $3, - ai_model = $4, - temperature = $5, + theme = EXCLUDED.theme, + font_size = EXCLUDED.font_size, + ai_model = EXCLUDED.ai_model, + temperature = EXCLUDED.temperature, updated_at = CURRENT_TIMESTAMP`, [username, settings.theme, settings.fontSize, settings.model, settings.temperature] ); @@ -281,19 +247,8 @@ const db = { // AI Service Integrations const aiServices = { - - formatResponse: (responseText, provider) => { - const identityQuestions = [ - /who\s*(are|is)\s*you/i, - /what\s*(are|is)\s*you/i, - /your\s*(name|identity)/i, - /introduce\s*yourself/i, - /are\s*you\s*(chatgpt|gemini|ai|bot)/i - ]; - - const isIdentityQuestion = identityQuestions.some(regex => regex.test(responseText)); - - if (isIdentityQuestion) { + formatResponse: (responseText) => { + if (isIdentityQuestion(responseText)) { return `I'm a general purpose AI assistant developed by Muhammad Bin Khalid and Maaz Waheed at MBK Tech Studio. How can I help you today? ${responseText}`; } return responseText; @@ -301,6 +256,7 @@ const aiServices = { gemini: async (apiKey, model, conversationHistory, temperature) => { const url = `https://generativelanguage.googleapis.com/v1/models/${model}:generateContent?key=${apiKey}`; + try { const response = await fetch(url, { method: 'POST', @@ -324,7 +280,7 @@ const aiServices = { const data = await response.json(); const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text || "No response from Gemini API"; - return aiServices.formatResponse(responseText, 'gemini'); + return aiServices.formatResponse(responseText); } catch (error) { console.error("Gemini API error:", error); throw error; @@ -333,6 +289,7 @@ const aiServices = { mallow: async (prompt) => { const url = "https://literate-slightly-seahorse.ngrok-free.app/generate"; + try { const response = await fetch(url, { method: 'POST', @@ -347,14 +304,14 @@ const aiServices = { const data = await response.json(); const responseText = data.response || "No response from Mallow API"; - return aiServices.formatResponse(responseText, 'mallow'); + return aiServices.formatResponse(responseText); } catch (error) { console.error("Mallow API error:", error); return "API service is not available. Please contact [Maaz Waheed](https://github.com/42Wor) to start the API service."; } }, - nvidia: async (apiKey, model, conversationHistory, temperature) => { + nvidia: async (apiKey, model, conversationHistory) => { const url = "https://integrate.api.nvidia.com/v1/chat/completions"; const formattedHistory = conversationHistory .map(message => ({ @@ -373,7 +330,7 @@ const aiServices = { body: JSON.stringify({ model, messages: formattedHistory, - temperature: 0.5, // Use a fixed temperature for NVIDIA API + temperature: 0.5, top_p: 1.0, max_tokens: 4096, stream: false @@ -394,7 +351,7 @@ const aiServices = { const data = JSON.parse(responseBody); const responseText = data.choices?.[0]?.message?.content || "No response from NVIDIA API"; - return aiServices.formatResponse(responseText, 'nvidia'); + return aiServices.formatResponse(responseText); } catch (error) { console.error("NVIDIA API error:", error); throw error; @@ -402,6 +359,7 @@ const aiServices = { } }; +// Routes router.get(["/login", "/signin"], (req, res) => { res.render("staticPage/login.handlebars", { userLoggedIn: !!req.session?.user, @@ -413,6 +371,24 @@ router.get("/chatbot/:chatId?", validateSessionAndRole("Any"), async (req, res) try { const { chatId } = req.params; const { username, role } = req.session.user; + + let chatHistory = null; + let isChatIdOfOtherUser = false; + + if (chatId) { + chatHistory = await db.fetchChatHistoryById(chatId); + + if (!chatHistory) { + return res.status(404).render("templates/Error/Error.handlebars", { error: "Chat ID not found", errorCode: 404 }); + } + + if (chatHistory.username !== username && role !== "SuperAdmin") { + return res.status(403).render("templates/Error/Error.handlebars", { error: "Access denied for this Chat ID", errorCode: 403 }); + } + + isChatIdOfOtherUser = (chatHistory.username !== username); + } + const userSettings = await db.fetchUserSettings(username); res.render('mainPages/chatbot.handlebars', { @@ -422,11 +398,13 @@ router.get("/chatbot/:chatId?", validateSessionAndRole("Any"), async (req, res) temperature_value: (userSettings.temperature || DEFAULT_TEMPERATURE).toFixed(1) }, UserName: username, - role + role, + isChatIdOfOtherUser, + chatIdUsername: chatHistory?.username || null, }); } catch (error) { console.error("Error rendering chatbot page:", error); - res.status(500).render("templates/Error/500", { error: error.message }); + res.status(500).render("templates/Error/500.handlebars", { error: "Internal Server Error" }); } }); @@ -453,128 +431,12 @@ router.get('/api/chat/histories/:chatId', validateSessionAndRole("Any"), async ( try { const chatHistory = await db.fetchChatHistoryById(chatId); - chatHistory - ? res.json(chatHistory) - : res.status(404).json({ message: "Chat history not found" }); - } catch (error) { - handleApiError(res, error, "fetching chat history by ID"); - } -}); - -router.get('/admin/chatbot/gemini', validateSessionAndRole("SuperAdmin"), async (req, res) => { - try { - if (googleAuthError) { - return res.status(500).render("templates/Error/500", { - error: "Google API client initialization failed", - details: googleAuthError.message - }); - } - - if (!serviceusage || !projectId) { - return res.status(500).render("templates/Error/500", { - error: "Google Service Usage client or Project ID not available" - }); - } - - const geminiApiKey = process.env.GEMINI_API_KEY_maaz_waheed; - if (!geminiApiKey) { - return res.status(500).render("templates/Error/500", { - error: "Gemini API Key not configured" - }); - } - - const geminiModels = [ - 'gemini-2.0-flash', - 'gemini-2.0-flash-lite', - 'gemini-1.5-flash', - 'gemini-1.5-flash-8b', - 'gemini-1.5-pro' - ]; - - const modelData = await Promise.all(geminiModels.map(async (model) => { - try { - const modelInfoUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}?key=${geminiApiKey}`; - const infoResponse = await fetch(modelInfoUrl); - - if (!infoResponse.ok) { - return { - name: model, - available: false, - error: `Model info not available (${infoResponse.status})` - }; - } - - const infoData = await infoResponse.json(); - return { - name: model, - available: true, - description: infoData.description || 'No description', - inputTokenLimit: infoData.inputTokenLimit || 'Unknown', - outputTokenLimit: infoData.outputTokenLimit || 'Unknown', - supportedMethods: infoData.supportedGenerationMethods || [], - lastTested: new Date().toISOString() - }; - } catch (error) { - return { - name: model, - available: false, - error: error.message - }; - } - })); - - let quotaInfo = {}; - try { - const quotas = await serviceusage.services.consumerQuotaMetrics.list({ - parent: `projects/${projectId}/services/generativelanguage.googleapis.com` - }); - - quotaInfo = { - metrics: quotas.data?.metrics?.map(metric => ({ - displayName: metric.displayName || metric.name || 'Unknown Metric', - name: metric.name, - limit: metric.consumerQuotaLimits?.[0]?.quotaBuckets?.[0]?.effectiveLimit || 'N/A', - usage: metric.consumerQuotaLimits?.[0]?.quotaBuckets?.[0]?.currentUsage || 0, - percentage: 'N/A' // Calculated in template - })) || [], - updatedAt: new Date().toISOString() - }; - } catch (error) { - quotaInfo = { - error: true, - message: "Failed to fetch quota information", - details: error.message - }; + if (!chatHistory) { + return res.status(404).json({ message: "Chat history not found" }); } - - res.render("mainPages/geminiDashboard", { - title: "Gemini API Dashboard", - models: modelData.filter(m => m.available), - unavailableModels: modelData.filter(m => !m.available), - quotaInfo, - lastUpdated: new Date().toISOString(), - apiKeyConfigured: !!geminiApiKey, - googleAuthError: googleAuthError?.message, - projectId, - helpers: { - json: (context) => JSON.stringify(context, null, 2), - join: (array, separator) => Array.isArray(array) ? array.join(separator) : 'None', - formatNumber: (num) => (typeof num === 'number' || !isNaN(Number(num))) - ? Number(num).toLocaleString() - : num?.toString() || '0', - findMetric: (metrics, name) => Array.isArray(metrics) - ? (metrics.find(m => m.name === name) || { name, usage: 'Not found', limit: 'N/A', percentage: 'N/A' }) - : { name, usage: 'Metrics unavailable', limit: 'N/A', percentage: 'N/A' }, - isError: (obj) => obj && obj.error, - formatDate: (isoString) => isoString ? new Date(isoString).toLocaleString() : 'N/A' - } - }); + res.json(chatHistory); } catch (error) { - console.error("Error in Gemini dashboard:", error); - res.status(500).render("templates/Error/500", { - error: "Failed to retrieve Gemini API information", - details: error.message - }); + handleApiError(res, error, "fetching chat history by ID"); } }); @@ -582,44 +444,34 @@ router.post('/api/chat/delete-message/:chatId', validateSessionAndRole("Any"), a const { chatId } = req.params; const { messageId } = req.body; - console.log(`Request received to delete message. Chat ID: ${chatId}, Message ID: ${messageId}`); - - if (!chatId) { - console.error("Chat ID is missing in the request"); - return res.status(400).json({ message: "Chat ID is required" }); - } - if (!messageId) { - console.error("Message ID is missing in the request"); - return res.status(400).json({ message: "Message ID is required" }); + if (!chatId || !messageId) { + return res.status(400).json({ + message: "Chat ID and Message ID are required" + }); } try { - console.log(`Fetching chat history for Chat ID: ${chatId}`); const chatHistory = await db.fetchChatHistoryById(chatId); if (!chatHistory) { - console.error(`Chat history not found for Chat ID: ${chatId}`); return res.status(404).json({ message: "Chat history not found" }); } - console.log(`Parsing conversation history for Chat ID: ${chatId}`); let conversationHistory = typeof chatHistory.conversation_history === 'string' ? JSON.parse(chatHistory.conversation_history) : chatHistory.conversation_history; - console.log(`Original conversation history length: ${conversationHistory.length}`); - conversationHistory = conversationHistory.filter((msg, index) => index.toString() !== messageId); - console.log(`Updated conversation history length: ${conversationHistory.length}`); + conversationHistory = conversationHistory.filter((_, index) => + index.toString() !== messageId + ); - console.log(`Saving updated conversation history for Chat ID: ${chatId}`); await pool.query( 'UPDATE ai_history_chatapi SET conversation_history = $1 WHERE id = $2', [JSON.stringify(conversationHistory), chatId] ); - console.log(`Message with ID: ${messageId} successfully deleted from Chat ID: ${chatId}`); res.json({ success: true, message: "Message deleted" }); } catch (error) { - console.error(`Error deleting message with ID: ${messageId} from Chat ID: ${chatId}`, error); + console.error(`Error deleting message from chat ${chatId}:`, error); handleApiError(res, error, "deleting message"); } }); @@ -629,58 +481,62 @@ router.post('/api/bot-chat', checkMessageLimit, async (req, res) => { const { username } = req.session.user; try { - // Get user settings const userSettings = await db.fetchUserSettings(username); const temperature = Math.min(Math.max(parseFloat(userSettings.temperature || DEFAULT_TEMPERATURE), 0), 2); + const [provider, model] = userSettings.ai_model.includes('/') + ? userSettings.ai_model.split('/') + : ['gemini', userSettings.ai_model]; - // Get conversation history let conversationHistory = []; if (chatId) { const fetchedHistory = await db.fetchChatHistoryById(chatId); + if (!fetchedHistory && chatId) { + return res.status(404).json({ message: "Chat history not found" }); + } if (fetchedHistory?.conversation_history) { conversationHistory = typeof fetchedHistory.conversation_history === 'string' ? JSON.parse(fetchedHistory.conversation_history) : fetchedHistory.conversation_history; - } else if (chatId) { - return res.status(404).json({ message: "Chat history not found" }); } } else { - // Add system message for new chats conversationHistory.push({ - role: "user", // Changed from "system" to "user" + role: "user", parts: [{ - text: "IMPORTANT CONTEXT: You are an AI chatbot developed by Muhammad Bin Khalid and Maaz Waheed at MBK Tech Studio. You're a general purpose chatbot (not specifically about MBK Tech Studio). When asked about your identity, mention your developers and that you're a general AI assistant developed at MBK Tech Studio. Keep responses concise." + text: "IMPORTANT CONTEXT: You are an AI chatbot developed by Muhammad Bin Khalid and Maaz Waheed at MBK Tech Studio. You're a general purpose chatbot (not specifically about MBK Tech Studio). When asked about your identity, mention your developers and that you're a general AI assistant developed at MBK Tech Studio." }] }); } - // Add new user message conversationHistory.push({ role: "user", parts: [{ text: message }] }); - // Determine AI provider and model - const [provider, model] = userSettings.ai_model.includes('/') - ? userSettings.ai_model.split('/') - : ['gemini', userSettings.ai_model]; - - // Call appropriate AI service let aiResponseText; switch (provider.toLowerCase()) { case 'mallow': aiResponseText = await aiServices.mallow(message); break; case 'nvidia': - const nvidiaApiKey = process.env.NVIDIA_API; - if (!nvidiaApiKey) throw new Error("NVIDIA API key not configured"); - aiResponseText = await aiServices.nvidia(nvidiaApiKey, userSettings.ai_model, conversationHistory, temperature); + if (!process.env.NVIDIA_API) { + throw new Error("NVIDIA API key not configured"); + } + aiResponseText = await aiServices.nvidia( + process.env.NVIDIA_API, + userSettings.ai_model, + conversationHistory + ); break; case 'gemini': default: - const geminiApiKey = process.env.GEMINI_API_KEY_maaz_waheed; - if (!geminiApiKey) throw new Error("Gemini API key not configured"); - aiResponseText = await aiServices.gemini(geminiApiKey, model, conversationHistory, temperature); + if (!process.env.GEMINI_API_KEY_maaz_waheed) { + throw new Error("Gemini API key not configured"); + } + aiResponseText = await aiServices.gemini( + process.env.GEMINI_API_KEY_maaz_waheed, + model, + conversationHistory, + temperature + ); } - // Save conversation if not Mallow let newChatId = chatId; if (provider !== 'mallow') { conversationHistory.push({ role: "model", parts: [{ text: aiResponseText }] }); @@ -725,10 +581,11 @@ router.get('/api/user-settings', validateSessionAndRole("Any"), async (req, res) router.post('/api/save-settings', validateSessionAndRole("Any"), async (req, res) => { try { - const isSaved = await db.saveUserSettings(req.session.user.username, req.body); - isSaved - ? res.json({ success: true, message: "Settings saved" }) - : res.status(500).json({ success: false, message: "Failed to save settings" }); + const success = await db.saveUserSettings(req.session.user.username, req.body); + res.json({ + success, + message: success ? "Settings saved" : "Failed to save settings" + }); } catch (error) { handleApiError(res, error, "saving settings"); } diff --git a/views/admin/chats.handlebars b/views/admin/chats.handlebars new file mode 100644 index 0000000..23aad4e --- /dev/null +++ b/views/admin/chats.handlebars @@ -0,0 +1,256 @@ +
+ + + +| User | +Messages | +Activity | +
|---|---|---|
|
+ {{this.username}}
+ {{this.active_days}} active days
+ |
+ {{this.total_messages}} | ++ Peak: {{this.peak_messages_in_day}}/day + | +
| Model | +Users | +Avg Temp | +
|---|---|---|
| {{this.ai_model}} | +{{this.count}} | ++ {{this.avg_temp}} + Limit: {{this.avg_limit}} + | +
+ {{#if errorCode}}
+ Error Code: {{errorCode}}
+ {{/if}}