joklihuhiuyuiyhuiyyiuyiuj

This commit is contained in:
Slipstream 2025-05-03 17:44:55 -06:00
parent e551b48c8b
commit 156125e1db
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
8 changed files with 2858 additions and 135 deletions

View File

@ -0,0 +1,508 @@
/* Components CSS file for the Discord Bot Dashboard */
/* Loading Spinner */
.loading-spinner-container {
display: flex;
justify-content: center;
align-items: center;
padding: var(--spacing-6);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(88, 101, 242, 0.2);
border-radius: 50%;
border-top-color: var(--primary-color);
animation: spin 1s ease-in-out infinite;
}
.loading-spinner-sm {
width: 20px;
height: 20px;
border-width: 2px;
}
.loading-spinner-lg {
width: 60px;
height: 60px;
border-width: 6px;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Button with loading state */
.btn-loading {
position: relative;
pointer-events: none;
color: transparent !important;
}
.btn-loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
top: calc(50% - 8px);
left: calc(50% - 8px);
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
/* Toast Notifications */
.toast-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
.toast {
padding: var(--spacing-3) var(--spacing-4);
border-radius: var(--radius-md);
background-color: var(--card-bg);
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
gap: var(--spacing-3);
min-width: 300px;
max-width: 450px;
animation: slideIn 0.3s ease-out forwards;
}
.toast.toast-success {
border-left: 4px solid var(--success-color);
}
.toast.toast-error {
border-left: 4px solid var(--danger-color);
}
.toast.toast-warning {
border-left: 4px solid var(--warning-color);
}
.toast.toast-info {
border-left: 4px solid var(--primary-color);
}
.toast-icon {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.toast-content {
flex: 1;
}
.toast-title {
font-weight: 600;
margin-bottom: var(--spacing-1);
}
.toast-message {
color: var(--text-secondary);
font-size: 0.875rem;
}
.toast-close {
background: none;
border: none;
cursor: pointer;
color: var(--text-secondary);
font-size: 1.25rem;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.toast-close:hover {
color: var(--text-primary);
}
.toast.toast-hiding {
animation: slideOut 0.3s ease-in forwards;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
/* Badges */
.badge {
display: inline-block;
padding: 0.25em 0.6em;
font-size: 0.75rem;
font-weight: 600;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 10px;
}
.badge-primary {
background-color: var(--primary-color);
color: white;
}
.badge-secondary {
background-color: var(--secondary-color);
color: white;
}
.badge-success {
background-color: var(--success-color);
color: white;
}
.badge-warning {
background-color: var(--warning-color);
color: white;
}
.badge-danger {
background-color: var(--danger-color);
color: white;
}
.badge-light {
background-color: #e2e8f0;
color: var(--text-primary);
}
/* Tabs */
.tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
margin-bottom: var(--spacing-6);
}
.tab {
padding: var(--spacing-3) var(--spacing-4);
cursor: pointer;
border-bottom: 2px solid transparent;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s;
}
.tab:hover {
color: var(--primary-color);
}
.tab.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Modals */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
animation: fadeIn 0.3s;
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background-color: var(--card-bg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
width: 90%;
max-width: 500px;
animation: scaleIn 0.3s;
position: relative;
max-height: 90vh;
display: flex;
flex-direction: column;
}
.modal-header {
padding: var(--spacing-4) var(--spacing-6);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
}
.modal-close:hover {
background-color: rgba(0, 0, 0, 0.05);
color: var(--text-primary);
}
.modal-body {
padding: var(--spacing-6);
overflow-y: auto;
}
.modal-footer {
padding: var(--spacing-4) var(--spacing-6);
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: var(--spacing-3);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from {
transform: scale(0.9);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
/* Dropdowns */
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-toggle {
cursor: pointer;
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
z-index: 1000;
display: none;
min-width: 180px;
padding: var(--spacing-2) 0;
margin: var(--spacing-1) 0 0;
background-color: var(--card-bg);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
}
.dropdown-menu.show {
display: block;
animation: fadeIn 0.2s;
}
.dropdown-item {
display: block;
width: 100%;
padding: var(--spacing-2) var(--spacing-4);
clear: both;
font-weight: 400;
color: var(--text-primary);
text-align: inherit;
white-space: nowrap;
background-color: transparent;
border: 0;
text-decoration: none;
cursor: pointer;
}
.dropdown-item:hover {
background-color: rgba(0, 0, 0, 0.05);
text-decoration: none;
}
.dropdown-divider {
height: 0;
margin: var(--spacing-2) 0;
overflow: hidden;
border-top: 1px solid var(--border-color);
}
/* Alerts */
.alert {
position: relative;
padding: var(--spacing-4);
margin-bottom: var(--spacing-4);
border: 1px solid transparent;
border-radius: var(--radius-md);
}
.alert-success {
color: #0f5132;
background-color: #d1e7dd;
border-color: #badbcc;
}
.alert-danger {
color: #842029;
background-color: #f8d7da;
border-color: #f5c2c7;
}
.alert-warning {
color: #664d03;
background-color: #fff3cd;
border-color: #ffecb5;
}
.alert-info {
color: #055160;
background-color: #cff4fc;
border-color: #b6effb;
}
.alert-dismissible {
padding-right: 3rem;
}
.alert-dismissible .close {
position: absolute;
top: 0;
right: 0;
padding: var(--spacing-4);
color: inherit;
background: transparent;
border: 0;
cursor: pointer;
}
/* Progress Bar */
.progress {
display: flex;
height: 0.75rem;
overflow: hidden;
font-size: 0.75rem;
background-color: #e9ecef;
border-radius: var(--radius-md);
}
.progress-bar {
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
color: #fff;
text-align: center;
white-space: nowrap;
background-color: var(--primary-color);
transition: width 0.6s ease;
}
/* Tooltips */
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 120px;
background-color: var(--secondary-color);
color: #fff;
text-align: center;
border-radius: var(--radius-sm);
padding: var(--spacing-2);
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -60px;
opacity: 0;
transition: opacity 0.3s;
font-size: 0.75rem;
}
.tooltip .tooltip-text::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: var(--secondary-color) transparent transparent transparent;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}

View File

@ -0,0 +1,290 @@
/* Layout CSS file for the Discord Bot Dashboard */
/* Grid System */
.row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.col {
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
}
.col-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
.col-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
.col-3 { flex: 0 0 25%; max-width: 25%; }
.col-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
.col-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
.col-6 { flex: 0 0 50%; max-width: 50%; }
.col-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
.col-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
.col-9 { flex: 0 0 75%; max-width: 75%; }
.col-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
.col-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
.col-12 { flex: 0 0 100%; max-width: 100%; }
/* Responsive Breakpoints */
@media (max-width: 576px) {
.col-sm-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
.col-sm-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
.col-sm-3 { flex: 0 0 25%; max-width: 25%; }
.col-sm-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
.col-sm-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
.col-sm-6 { flex: 0 0 50%; max-width: 50%; }
.col-sm-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
.col-sm-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
.col-sm-9 { flex: 0 0 75%; max-width: 75%; }
.col-sm-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
.col-sm-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
.col-sm-12 { flex: 0 0 100%; max-width: 100%; }
}
@media (max-width: 768px) {
.col-md-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
.col-md-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
.col-md-3 { flex: 0 0 25%; max-width: 25%; }
.col-md-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
.col-md-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
.col-md-6 { flex: 0 0 50%; max-width: 50%; }
.col-md-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
.col-md-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
.col-md-9 { flex: 0 0 75%; max-width: 75%; }
.col-md-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
.col-md-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
.col-md-12 { flex: 0 0 100%; max-width: 100%; }
/* Mobile sidebar adjustments */
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.show {
transform: translateX(0);
}
.main-content {
margin-left: 0;
transition: margin-left 0.3s ease;
}
.header {
left: 0;
transition: left 0.3s ease;
}
.sidebar-toggle {
display: block !important;
}
}
@media (max-width: 992px) {
.col-lg-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
.col-lg-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
.col-lg-3 { flex: 0 0 25%; max-width: 25%; }
.col-lg-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
.col-lg-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
.col-lg-6 { flex: 0 0 50%; max-width: 50%; }
.col-lg-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
.col-lg-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
.col-lg-9 { flex: 0 0 75%; max-width: 75%; }
.col-lg-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
.col-lg-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
.col-lg-12 { flex: 0 0 100%; max-width: 100%; }
}
@media (max-width: 1200px) {
.col-xl-1 { flex: 0 0 8.333333%; max-width: 8.333333%; }
.col-xl-2 { flex: 0 0 16.666667%; max-width: 16.666667%; }
.col-xl-3 { flex: 0 0 25%; max-width: 25%; }
.col-xl-4 { flex: 0 0 33.333333%; max-width: 33.333333%; }
.col-xl-5 { flex: 0 0 41.666667%; max-width: 41.666667%; }
.col-xl-6 { flex: 0 0 50%; max-width: 50%; }
.col-xl-7 { flex: 0 0 58.333333%; max-width: 58.333333%; }
.col-xl-8 { flex: 0 0 66.666667%; max-width: 66.666667%; }
.col-xl-9 { flex: 0 0 75%; max-width: 75%; }
.col-xl-10 { flex: 0 0 83.333333%; max-width: 83.333333%; }
.col-xl-11 { flex: 0 0 91.666667%; max-width: 91.666667%; }
.col-xl-12 { flex: 0 0 100%; max-width: 100%; }
}
/* Sidebar Toggle Button (hidden by default, shown on mobile) */
.sidebar-toggle {
display: none;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-primary);
padding: 0;
margin-right: var(--spacing-4);
}
/* Responsive Containers */
.container-fluid {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
.container {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
@media (min-width: 576px) {
.container {
max-width: 540px;
}
}
@media (min-width: 768px) {
.container {
max-width: 720px;
}
}
@media (min-width: 992px) {
.container {
max-width: 960px;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1140px;
}
}
/* Flexbox Utilities */
.d-flex {
display: flex !important;
}
.flex-row {
flex-direction: row !important;
}
.flex-column {
flex-direction: column !important;
}
.flex-wrap {
flex-wrap: wrap !important;
}
.flex-nowrap {
flex-wrap: nowrap !important;
}
.justify-content-start {
justify-content: flex-start !important;
}
.justify-content-end {
justify-content: flex-end !important;
}
.justify-content-center {
justify-content: center !important;
}
.justify-content-between {
justify-content: space-between !important;
}
.justify-content-around {
justify-content: space-around !important;
}
.align-items-start {
align-items: flex-start !important;
}
.align-items-end {
align-items: flex-end !important;
}
.align-items-center {
align-items: center !important;
}
.align-items-baseline {
align-items: baseline !important;
}
.align-items-stretch {
align-items: stretch !important;
}
/* Spacing Utilities */
.m-0 { margin: 0 !important; }
.mt-0 { margin-top: 0 !important; }
.mr-0 { margin-right: 0 !important; }
.mb-0 { margin-bottom: 0 !important; }
.ml-0 { margin-left: 0 !important; }
.p-0 { padding: 0 !important; }
.pt-0 { padding-top: 0 !important; }
.pr-0 { padding-right: 0 !important; }
.pb-0 { padding-bottom: 0 !important; }
.pl-0 { padding-left: 0 !important; }
/* Display Utilities */
.d-none {
display: none !important;
}
.d-inline {
display: inline !important;
}
.d-inline-block {
display: inline-block !important;
}
.d-block {
display: block !important;
}
@media (min-width: 576px) {
.d-sm-none { display: none !important; }
.d-sm-inline { display: inline !important; }
.d-sm-inline-block { display: inline-block !important; }
.d-sm-block { display: block !important; }
.d-sm-flex { display: flex !important; }
}
@media (min-width: 768px) {
.d-md-none { display: none !important; }
.d-md-inline { display: inline !important; }
.d-md-inline-block { display: inline-block !important; }
.d-md-block { display: block !important; }
.d-md-flex { display: flex !important; }
}
@media (min-width: 992px) {
.d-lg-none { display: none !important; }
.d-lg-inline { display: inline !important; }
.d-lg-inline-block { display: inline-block !important; }
.d-lg-block { display: block !important; }
.d-lg-flex { display: flex !important; }
}
@media (min-width: 1200px) {
.d-xl-none { display: none !important; }
.d-xl-inline { display: inline !important; }
.d-xl-inline-block { display: inline-block !important; }
.d-xl-block { display: block !important; }
.d-xl-flex { display: flex !important; }
}

View File

@ -0,0 +1,470 @@
/* Main CSS file for the Discord Bot Dashboard */
:root {
/* Color variables */
--primary-color: #5865F2; /* Discord blue */
--primary-hover: #4752C4;
--secondary-color: #2D3748;
--accent-color: #7289DA;
--success-color: #48BB78;
--warning-color: #F6AD55;
--danger-color: #F56565;
--light-bg: #F7FAFC;
--dark-bg: #1A202C;
--card-bg: #FFFFFF;
--text-primary: #2D3748;
--text-secondary: #718096;
--border-color: #E2E8F0;
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
/* Spacing variables */
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-3: 0.75rem;
--spacing-4: 1rem;
--spacing-6: 1.5rem;
--spacing-8: 2rem;
--spacing-12: 3rem;
--spacing-16: 4rem;
}
/* Base styles */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: var(--light-bg);
color: var(--text-primary);
line-height: 1.5;
min-height: 100vh;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.25;
margin-bottom: var(--spacing-4);
color: var(--secondary-color);
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
h4 {
font-size: 1rem;
}
a {
color: var(--primary-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Layout */
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--spacing-4);
}
.dashboard-container {
display: flex;
min-height: 100vh;
}
.main-content {
flex: 1;
padding: var(--spacing-6);
margin-left: 250px; /* Width of the sidebar */
}
/* Header */
.header {
background-color: var(--card-bg);
border-bottom: 1px solid var(--border-color);
padding: var(--spacing-4) var(--spacing-6);
display: flex;
justify-content: space-between;
align-items: center;
position: fixed;
top: 0;
right: 0;
left: 250px; /* Width of the sidebar */
height: 64px;
z-index: 10;
box-shadow: var(--shadow-sm);
}
.header-title {
font-size: 1.25rem;
margin: 0;
}
.user-info {
display: flex;
align-items: center;
gap: var(--spacing-3);
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
}
.user-name {
font-weight: 500;
}
/* Sidebar */
.sidebar {
width: 250px;
background-color: var(--secondary-color);
color: white;
position: fixed;
top: 0;
left: 0;
bottom: 0;
overflow-y: auto;
z-index: 20;
}
.sidebar-header {
padding: var(--spacing-6);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-logo {
font-size: 1.5rem;
font-weight: 700;
color: white;
text-decoration: none;
}
.sidebar-nav {
padding: var(--spacing-4) 0;
}
.nav-item {
padding: var(--spacing-3) var(--spacing-6);
display: flex;
align-items: center;
gap: var(--spacing-3);
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
transition: background-color 0.2s;
}
.nav-item:hover {
background-color: rgba(255, 255, 255, 0.1);
color: white;
text-decoration: none;
}
.nav-item.active {
background-color: var(--primary-color);
color: white;
}
.nav-icon {
width: 20px;
height: 20px;
}
.sidebar-footer {
padding: var(--spacing-4) var(--spacing-6);
border-top: 1px solid rgba(255, 255, 255, 0.1);
position: absolute;
bottom: 0;
width: 100%;
}
/* Cards */
.card {
background-color: var(--card-bg);
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
padding: var(--spacing-6);
margin-bottom: var(--spacing-6);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-4);
}
.card-title {
font-size: 1.25rem;
margin: 0;
}
/* Forms */
.form-group {
margin-bottom: var(--spacing-4);
}
label {
display: block;
margin-bottom: var(--spacing-2);
font-weight: 500;
}
input[type="text"],
input[type="number"],
input[type="password"],
input[type="email"],
input[type="search"],
select,
textarea {
width: 100%;
padding: var(--spacing-3);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background-color: white;
font-size: 1rem;
transition: border-color 0.2s;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(88, 101, 242, 0.2);
}
textarea {
resize: vertical;
min-height: 100px;
}
input[type="checkbox"],
input[type="radio"] {
margin-right: var(--spacing-2);
}
.checkbox-label,
.radio-label {
display: flex;
align-items: center;
cursor: pointer;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--spacing-2) var(--spacing-4);
border: none;
border-radius: var(--radius-md);
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
}
.btn:hover {
transform: translateY(-1px);
}
.btn:active {
transform: translateY(0);
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-hover);
}
.btn-secondary {
background-color: var(--secondary-color);
color: white;
}
.btn-secondary:hover {
background-color: #3a4a63;
}
.btn-success {
background-color: var(--success-color);
color: white;
}
.btn-success:hover {
background-color: #3da066;
}
.btn-warning {
background-color: var(--warning-color);
color: white;
}
.btn-warning:hover {
background-color: #e09c49;
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn-danger:hover {
background-color: #e04c4c;
}
.btn-sm {
font-size: 0.875rem;
padding: var(--spacing-1) var(--spacing-3);
}
.btn-lg {
font-size: 1.125rem;
padding: var(--spacing-3) var(--spacing-6);
}
.btn-icon {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
}
.btn-group {
display: flex;
gap: var(--spacing-3);
}
/* Utilities */
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.mt-1 { margin-top: var(--spacing-1); }
.mt-2 { margin-top: var(--spacing-2); }
.mt-4 { margin-top: var(--spacing-4); }
.mt-6 { margin-top: var(--spacing-6); }
.mb-1 { margin-bottom: var(--spacing-1); }
.mb-2 { margin-bottom: var(--spacing-2); }
.mb-4 { margin-bottom: var(--spacing-4); }
.mb-6 { margin-bottom: var(--spacing-6); }
.ml-1 { margin-left: var(--spacing-1); }
.ml-2 { margin-left: var(--spacing-2); }
.ml-4 { margin-left: var(--spacing-4); }
.ml-6 { margin-left: var(--spacing-6); }
.mr-1 { margin-right: var(--spacing-1); }
.mr-2 { margin-right: var(--spacing-2); }
.mr-4 { margin-right: var(--spacing-4); }
.mr-6 { margin-right: var(--spacing-6); }
.p-1 { padding: var(--spacing-1); }
.p-2 { padding: var(--spacing-2); }
.p-4 { padding: var(--spacing-4); }
.p-6 { padding: var(--spacing-6); }
.hidden {
display: none !important;
}
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.gap-2 {
gap: var(--spacing-2);
}
.gap-4 {
gap: var(--spacing-4);
}
.w-full {
width: 100%;
}
.text-sm {
font-size: 0.875rem;
}
.text-lg {
font-size: 1.125rem;
}
.font-bold {
font-weight: 700;
}
.text-gray {
color: var(--text-secondary);
}
.text-success {
color: var(--success-color);
}
.text-warning {
color: var(--warning-color);
}
.text-danger {
color: var(--danger-color);
}

View File

@ -4,49 +4,114 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bot Dashboard</title>
<link rel="stylesheet" href="style.css">
<!-- Preload fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- CSS files -->
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/components.css">
<link rel="stylesheet" href="css/layout.css">
</head>
<body>
<h1>Discord Bot Dashboard</h1>
<div id="auth-section">
<button id="login-button">Login with Discord</button>
<!-- Auth Section -->
<div id="auth-section" class="container">
<div class="card mt-6" style="max-width: 500px; margin: 100px auto; text-align: center;">
<h1>Discord Bot Dashboard</h1>
<p class="mb-4">Manage your Discord bot settings and configurations</p>
<button id="login-button" class="btn btn-primary btn-lg">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
Login with Discord
</button>
</div>
</div>
<div id="dashboard-section" style="display: none;">
<h2>Welcome, <span id="username">User</span>!</h2>
<button id="logout-button">Logout</button>
<div class="dashboard-nav">
<button id="nav-server-settings" class="nav-button active">Server Settings</button>
<button id="nav-ai-settings" class="nav-button">AI Settings</button>
<button id="nav-conversations" class="nav-button">Conversations</button>
<!-- Dashboard Section -->
<div id="dashboard-container" class="dashboard-container" style="display: none;">
<!-- Sidebar -->
<div id="sidebar" class="sidebar">
<div class="sidebar-header">
<a href="#" class="sidebar-logo">Bot Dashboard</a>
</div>
<div class="sidebar-nav">
<a href="#server-settings" class="nav-item active" data-section="server-settings-section">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
Server Settings
</a>
<a href="#ai-settings" class="nav-item" data-section="ai-settings-section">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><path d="M12 2a10 10 0 1 0 10 10H12V2z"></path><path d="M20 12a8 8 0 1 0-16 0"></path><path d="M12 12v10"></path></svg>
AI Settings
</a>
<a href="#conversations" class="nav-item" data-section="conversations-section">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="nav-icon"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
Conversations
</a>
</div>
<div class="sidebar-footer">
<button id="logout-button" class="btn btn-danger w-full">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>
Logout
</button>
</div>
</div>
<hr>
<!-- Main Content -->
<div class="main-content">
<!-- Header -->
<header class="header">
<button id="sidebar-toggle" class="sidebar-toggle">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
</button>
<h1 class="header-title">Discord Bot Dashboard</h1>
<div class="user-info">
<div id="user-avatar" class="user-avatar">U</div>
<span id="username" class="user-name">User</span>
</div>
</header>
<!-- Content Container -->
<div class="content-container" style="margin-top: 80px;">
<!-- Server Settings Section -->
<div id="server-settings-section" class="dashboard-section">
<h3>Manage Server Settings</h3>
<label for="guild-select">Select Server:</label>
<select name="guilds" id="guild-select">
<option value="">--Please choose a server--</option>
</select>
<div class="card">
<div class="card-header">
<h2 class="card-title">Manage Server Settings</h2>
</div>
<div class="form-group">
<label for="guild-select">Select Server:</label>
<select name="guilds" id="guild-select" class="w-full">
<option value="">--Please choose a server--</option>
</select>
</div>
</div>
<div id="settings-form" style="display: none;">
<h4>Prefix</h4>
<label for="prefix-input">Command Prefix:</label>
<input type="text" id="prefix-input" name="prefix" maxlength="10">
<button id="save-prefix-button">Save Prefix</button>
<p id="prefix-feedback"></p>
<div class="card">
<div class="card-header">
<h3 class="card-title">Prefix Settings</h3>
</div>
<div class="form-group">
<label for="prefix-input">Command Prefix:</label>
<div class="flex gap-2">
<input type="text" id="prefix-input" name="prefix" maxlength="10" class="w-full">
<button id="save-prefix-button" class="btn btn-primary">
Save Prefix
</button>
</div>
<p id="prefix-feedback" class="mt-2"></p>
</div>
</div>
<h4>Welcome Messages</h4>
<div class="settings-card">
<div class="card">
<div class="card-header">
<h3 class="card-title">Welcome Messages</h3>
</div>
<div class="form-group">
<label for="welcome-channel">Welcome Channel:</label>
<div class="channel-select-container">
<input type="text" id="welcome-channel" name="welcome_channel_id" placeholder="Enter Channel ID">
<select id="welcome-channel-select" class="channel-dropdown">
<div class="flex gap-2">
<input type="text" id="welcome-channel" name="welcome_channel_id" placeholder="Enter Channel ID" class="w-full">
<select id="welcome-channel-select" class="w-full">
<option value="">-- Select Channel --</option>
<!-- Will be populated by JS -->
</select>
@ -54,22 +119,24 @@
</div>
<div class="form-group">
<label for="welcome-message">Welcome Message Template:</label>
<textarea id="welcome-message" name="welcome_message" rows="4" cols="50" placeholder="Use {user} for mention, {username} for name, {server} for server name."></textarea>
<textarea id="welcome-message" name="welcome_message" rows="4" placeholder="Use {user} for mention, {username} for name, {server} for server name." class="w-full"></textarea>
</div>
<div class="button-group">
<button id="save-welcome-button">Save Welcome Settings</button>
<button id="disable-welcome-button">Disable Welcome</button>
<div class="btn-group">
<button id="save-welcome-button" class="btn btn-primary">Save Welcome Settings</button>
<button id="disable-welcome-button" class="btn btn-warning">Disable Welcome</button>
</div>
<p id="welcome-feedback"></p>
<p id="welcome-feedback" class="mt-2"></p>
</div>
<h4>Goodbye Messages</h4>
<div class="settings-card">
<div class="card">
<div class="card-header">
<h3 class="card-title">Goodbye Messages</h3>
</div>
<div class="form-group">
<label for="goodbye-channel">Goodbye Channel:</label>
<div class="channel-select-container">
<input type="text" id="goodbye-channel" name="goodbye_channel_id" placeholder="Enter Channel ID">
<select id="goodbye-channel-select" class="channel-dropdown">
<div class="flex gap-2">
<input type="text" id="goodbye-channel" name="goodbye_channel_id" placeholder="Enter Channel ID" class="w-full">
<select id="goodbye-channel-select" class="w-full">
<option value="">-- Select Channel --</option>
<!-- Will be populated by JS -->
</select>
@ -77,64 +144,79 @@
</div>
<div class="form-group">
<label for="goodbye-message">Goodbye Message Template:</label>
<textarea id="goodbye-message" name="goodbye_message" rows="4" cols="50" placeholder="Use {username} for name, {server} for server name."></textarea>
<textarea id="goodbye-message" name="goodbye_message" rows="4" placeholder="Use {username} for name, {server} for server name." class="w-full"></textarea>
</div>
<div class="button-group">
<button id="save-goodbye-button">Save Goodbye Settings</button>
<button id="disable-goodbye-button">Disable Goodbye</button>
<div class="btn-group">
<button id="save-goodbye-button" class="btn btn-primary">Save Goodbye Settings</button>
<button id="disable-goodbye-button" class="btn btn-warning">Disable Goodbye</button>
</div>
<p id="goodbye-feedback"></p>
<p id="goodbye-feedback" class="mt-2"></p>
</div>
<h4>Enabled Modules (Cogs)</h4>
<div class="settings-card">
<div class="cogs-container">
<div class="card">
<div class="card-header">
<h3 class="card-title">Enabled Modules (Cogs)</h3>
</div>
<div class="cogs-container p-4 border rounded mb-4">
<div id="cogs-list">
<!-- Cog checkboxes will be populated by JS -->
<div class="loading-spinner-container">
<div class="loading-spinner"></div>
</div>
</div>
</div>
<div class="button-group">
<button id="save-cogs-button">Save Module Settings</button>
<div class="btn-group">
<button id="save-cogs-button" class="btn btn-primary">Save Module Settings</button>
</div>
<p id="cogs-feedback"></p>
<p id="cogs-feedback" class="mt-2"></p>
</div>
<h4>Command Permissions</h4>
<div class="settings-card">
<div class="card">
<div class="card-header">
<h3 class="card-title">Command Permissions</h3>
</div>
<div class="form-group">
<label for="command-select">Command:</label>
<select id="command-select">
<select id="command-select" class="w-full">
<!-- Will be populated by JS -->
<option value="">-- Select Command --</option>
</select>
</div>
<div class="form-group">
<label for="role-select">Role:</label>
<select id="role-select">
<select id="role-select" class="w-full">
<!-- Will be populated by JS -->
<option value="">-- Select Role --</option>
</select>
</div>
<div class="button-group">
<button id="add-perm-button">Allow Role</button>
<button id="remove-perm-button">Disallow Role</button>
<div class="btn-group">
<button id="add-perm-button" class="btn btn-success">Allow Role</button>
<button id="remove-perm-button" class="btn btn-danger">Disallow Role</button>
</div>
<div id="current-perms">
<div id="current-perms" class="mt-4 p-4 border rounded">
<!-- Current permissions will be listed here -->
<div class="text-gray">No permissions set yet.</div>
</div>
<p id="perms-feedback"></p>
<p id="perms-feedback" class="mt-2"></p>
</div>
</div>
</div>
<!-- AI Settings Section -->
<div id="ai-settings-section" class="dashboard-section" style="display: none;">
<h3>AI Settings</h3>
<div class="settings-card">
<h4>General AI Settings</h4>
<div class="card">
<div class="card-header">
<h2 class="card-title">AI Settings</h2>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">General AI Settings</h3>
</div>
<div class="form-group">
<label for="ai-model-select">AI Model:</label>
<select id="ai-model-select">
<select id="ai-model-select" class="w-full">
<option value="openai/gpt-3.5-turbo">GPT-3.5 Turbo</option>
<option value="openai/gpt-4">GPT-4</option>
<option value="anthropic/claude-3-opus">Claude 3 Opus</option>
@ -146,131 +228,196 @@
</div>
<div class="form-group">
<label for="ai-temperature">Temperature: <span id="temperature-value">0.7</span></label>
<input type="range" id="ai-temperature" min="0" max="2" step="0.1" value="0.7">
<input type="range" id="ai-temperature" min="0" max="2" step="0.1" value="0.7" class="w-full">
</div>
<div class="form-group">
<label for="ai-max-tokens">Max Tokens:</label>
<input type="number" id="ai-max-tokens" min="100" max="8000" step="100" value="1000">
<input type="number" id="ai-max-tokens" min="100" max="8000" step="100" value="1000" class="w-full">
</div>
<div class="form-group">
<label>
<label class="checkbox-label">
<input type="checkbox" id="ai-reasoning-enabled">
Enable Reasoning
</label>
</div>
<div class="form-group" id="reasoning-effort-group">
<label for="ai-reasoning-effort">Reasoning Effort:</label>
<select id="ai-reasoning-effort">
<select id="ai-reasoning-effort" class="w-full">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
</select>
</div>
<div class="form-group">
<label>
<label class="checkbox-label">
<input type="checkbox" id="ai-web-search-enabled">
Enable Web Search
</label>
</div>
<div class="button-group">
<button id="save-ai-settings-button">Save AI Settings</button>
<button id="reset-ai-settings-button">Reset to Defaults</button>
<div class="btn-group">
<button id="save-ai-settings-button" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
Save AI Settings
</button>
<button id="reset-ai-settings-button" class="btn btn-warning">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path></svg>
Reset to Defaults
</button>
</div>
<p id="ai-settings-feedback"></p>
<p id="ai-settings-feedback" class="mt-2"></p>
</div>
<div class="settings-card">
<h4>Character Settings</h4>
<div class="card">
<div class="card-header">
<h3 class="card-title">Character Settings</h3>
</div>
<div class="form-group">
<label for="ai-character">Character Name:</label>
<input type="text" id="ai-character" placeholder="e.g., Kasane Teto">
<input type="text" id="ai-character" placeholder="e.g., Kasane Teto" class="w-full">
</div>
<div class="form-group">
<label for="ai-character-info">Character Information:</label>
<textarea id="ai-character-info" rows="4" placeholder="Describe the character's personality, background, etc."></textarea>
<textarea id="ai-character-info" rows="4" placeholder="Describe the character's personality, background, etc." class="w-full"></textarea>
</div>
<div class="form-group">
<label>
<label class="checkbox-label">
<input type="checkbox" id="ai-character-breakdown">
Enable Character Breakdown
</label>
</div>
<div class="button-group">
<button id="save-character-settings-button">Save Character Settings</button>
<button id="clear-character-settings-button">Clear Character</button>
<div class="btn-group">
<button id="save-character-settings-button" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
Save Character Settings
</button>
<button id="clear-character-settings-button" class="btn btn-warning">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
Clear Character
</button>
</div>
<p id="character-settings-feedback"></p>
<p id="character-settings-feedback" class="mt-2"></p>
</div>
<div class="settings-card">
<h4>System Prompt</h4>
<div class="card">
<div class="card-header">
<h3 class="card-title">System Prompt</h3>
</div>
<div class="form-group">
<textarea id="ai-system-prompt" rows="6" placeholder="Enter a system prompt to guide the AI's behavior..."></textarea>
<textarea id="ai-system-prompt" rows="6" placeholder="Enter a system prompt to guide the AI's behavior..." class="w-full"></textarea>
</div>
<div class="button-group">
<button id="save-system-prompt-button">Save System Prompt</button>
<button id="reset-system-prompt-button">Reset to Default</button>
<div class="btn-group">
<button id="save-system-prompt-button" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
Save System Prompt
</button>
<button id="reset-system-prompt-button" class="btn btn-warning">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path></svg>
Reset to Default
</button>
</div>
<p id="system-prompt-feedback"></p>
<p id="system-prompt-feedback" class="mt-2"></p>
</div>
<div class="settings-card">
<h4>Custom Instructions</h4>
<div class="card">
<div class="card-header">
<h3 class="card-title">Custom Instructions</h3>
</div>
<div class="form-group">
<textarea id="ai-custom-instructions" rows="6" placeholder="Enter custom instructions for the AI..."></textarea>
<textarea id="ai-custom-instructions" rows="6" placeholder="Enter custom instructions for the AI..." class="w-full"></textarea>
</div>
<div class="button-group">
<button id="save-custom-instructions-button">Save Custom Instructions</button>
<button id="clear-custom-instructions-button">Clear Instructions</button>
<div class="btn-group">
<button id="save-custom-instructions-button" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
Save Custom Instructions
</button>
<button id="clear-custom-instructions-button" class="btn btn-warning">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
Clear Instructions
</button>
</div>
<p id="custom-instructions-feedback"></p>
<p id="custom-instructions-feedback" class="mt-2"></p>
</div>
</div>
<!-- Conversations Section -->
<div id="conversations-section" class="dashboard-section" style="display: none;">
<h3>AI Conversations</h3>
<div class="conversations-header">
<div class="search-container">
<input type="text" id="conversation-search" placeholder="Search conversations...">
</div>
<button id="new-conversation-button">New Conversation</button>
</div>
<div class="conversations-list-container">
<div id="conversations-list">
<!-- Will be populated by JS -->
<div class="no-conversations">No conversations found. Start a new conversation!</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">AI Conversations</h2>
</div>
</div>
<div id="conversation-detail" style="display: none;">
<div class="conversation-header">
<h4 id="conversation-title">Conversation Title</h4>
<div class="conversation-actions">
<button id="rename-conversation-button">Rename</button>
<button id="delete-conversation-button">Delete</button>
<button id="export-conversation-button">Export</button>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="card-title mb-0">Your Conversations</h3>
<button id="new-conversation-button" class="btn btn-primary btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
New Conversation
</button>
</div>
<div class="form-group mt-4">
<div class="search-container">
<input type="text" id="conversation-search" placeholder="Search conversations..." class="w-full">
</div>
</div>
<div class="conversation-messages" id="conversation-messages">
<!-- Will be populated by JS -->
<div class="conversations-list-container border rounded p-0 mb-4" style="max-height: 300px; overflow-y: auto;">
<div id="conversations-list">
<!-- Will be populated by JS -->
<div class="no-conversations p-4 text-center text-gray">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" style="margin: 0 auto 1rem;"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
<p>No conversations found.</p>
<button id="start-conversation-button" class="btn btn-primary btn-sm mt-2">Start a new conversation</button>
</div>
</div>
</div>
</div>
<div id="conversation-detail" class="card" style="display: none;">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 id="conversation-title" class="card-title mb-0">Conversation Title</h3>
<div class="conversation-actions">
<button id="rename-conversation-button" class="btn btn-secondary btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
</button>
<button id="export-conversation-button" class="btn btn-secondary btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
</button>
<button id="delete-conversation-button" class="btn btn-danger btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
</button>
</div>
</div>
<div class="conversation-messages p-4" id="conversation-messages" style="max-height: 500px; overflow-y: auto;">
<!-- Will be populated by JS -->
<div class="loading-spinner-container">
<div class="loading-spinner"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal for renaming conversations -->
<div id="rename-modal" class="modal">
<div class="modal-content">
<span class="close-modal">&times;</span>
<h4>Rename Conversation</h4>
<input type="text" id="new-conversation-title" placeholder="Enter new title">
<div class="button-group">
<button id="confirm-rename-button">Rename</button>
<button id="cancel-rename-button">Cancel</button>
<div class="modal-header">
<h3 class="modal-title">Rename Conversation</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="new-conversation-title">New Title:</label>
<input type="text" id="new-conversation-title" placeholder="Enter new title" class="w-full">
</div>
</div>
<div class="modal-footer">
<button id="cancel-rename-button" class="btn btn-secondary">Cancel</button>
<button id="confirm-rename-button" class="btn btn-primary">Rename</button>
</div>
</div>
</div>
@ -278,16 +425,42 @@
<!-- Modal for creating new conversations -->
<div id="new-conversation-modal" class="modal">
<div class="modal-content">
<span class="close-modal">&times;</span>
<h4>Create New Conversation</h4>
<input type="text" id="new-conversation-name" placeholder="Enter conversation title">
<div class="button-group">
<button id="create-conversation-button">Create</button>
<button id="cancel-create-button">Cancel</button>
<div class="modal-header">
<h3 class="modal-title">Create New Conversation</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="new-conversation-name">Conversation Title:</label>
<input type="text" id="new-conversation-name" placeholder="Enter conversation title" class="w-full">
</div>
</div>
<div class="modal-footer">
<button id="cancel-create-button" class="btn btn-secondary">Cancel</button>
<button id="create-conversation-button" class="btn btn-primary">Create</button>
</div>
</div>
</div>
<script src="script.js"></script>
<!-- Delete confirmation modal -->
<div id="delete-confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Delete Conversation</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this conversation? This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button id="cancel-delete-button" class="btn btn-secondary">Cancel</button>
<button id="confirm-delete-button" class="btn btn-danger">Delete</button>
</div>
</div>
</div>
<!-- JavaScript files -->
<script src="js/utils.js"></script>
<script src="js/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,723 @@
/**
* Main JavaScript file for the Discord Bot Dashboard
*/
// Initialize components when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// Initialize modals
Modal.init();
// Initialize sidebar toggle
initSidebar();
// Initialize authentication
initAuth();
// Initialize tabs
initTabs();
// Initialize dropdowns
initDropdowns();
});
/**
* Initialize sidebar functionality
*/
function initSidebar() {
const sidebarToggle = document.getElementById('sidebar-toggle');
const sidebar = document.getElementById('sidebar');
if (sidebarToggle && sidebar) {
sidebarToggle.addEventListener('click', () => {
sidebar.classList.toggle('show');
});
// Close sidebar when clicking outside on mobile
document.addEventListener('click', (event) => {
if (window.innerWidth <= 768 &&
sidebar.classList.contains('show') &&
!sidebar.contains(event.target) &&
event.target !== sidebarToggle) {
sidebar.classList.remove('show');
}
});
}
// Set active nav item based on current page
const currentPath = window.location.pathname;
document.querySelectorAll('.nav-item').forEach(item => {
const href = item.getAttribute('href');
if (href && currentPath.includes(href)) {
item.classList.add('active');
}
});
}
/**
* Initialize authentication
*/
function initAuth() {
const loginButton = document.getElementById('login-button');
const logoutButton = document.getElementById('logout-button');
const authSection = document.getElementById('auth-section');
const dashboardSection = document.getElementById('dashboard-container');
// Check authentication status
checkAuthStatus();
// Login button event
if (loginButton) {
loginButton.addEventListener('click', () => {
window.location.href = '/dashboard/api/auth/login';
});
}
// Logout button event
if (logoutButton) {
logoutButton.addEventListener('click', () => {
// Clear session
fetch('/dashboard/api/auth/logout', { method: 'POST' })
.then(() => {
// Redirect to login page
window.location.reload();
})
.catch(error => {
console.error('Logout error:', error);
Toast.error('Failed to logout. Please try again.');
});
});
}
/**
* Check if user is authenticated
*/
function checkAuthStatus() {
fetch('/dashboard/api/auth/status')
.then(response => response.json())
.then(data => {
if (data.authenticated) {
// User is authenticated, show dashboard
if (authSection) authSection.style.display = 'none';
if (dashboardSection) dashboardSection.style.display = 'block';
// Load user info
loadUserInfo();
// Load initial data
loadDashboardData();
} else {
// User is not authenticated, show login
if (authSection) authSection.style.display = 'block';
if (dashboardSection) dashboardSection.style.display = 'none';
}
})
.catch(error => {
console.error('Auth check error:', error);
// Assume not authenticated on error
if (authSection) authSection.style.display = 'block';
if (dashboardSection) dashboardSection.style.display = 'none';
});
}
/**
* Load user information
*/
function loadUserInfo() {
fetch('/dashboard/api/auth/user')
.then(response => response.json())
.then(user => {
// Update username display
const usernameSpan = document.getElementById('username');
if (usernameSpan) {
usernameSpan.textContent = user.username;
}
// Update avatar if available
const userAvatar = document.getElementById('user-avatar');
if (userAvatar) {
if (user.avatar) {
userAvatar.style.backgroundImage = `url(https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png)`;
userAvatar.textContent = '';
} else {
// Set initials as fallback
userAvatar.textContent = user.username.substring(0, 1).toUpperCase();
}
}
})
.catch(error => {
console.error('Error loading user info:', error);
});
}
}
/**
* Initialize tab functionality
*/
function initTabs() {
document.querySelectorAll('.tabs').forEach(tabContainer => {
const tabs = tabContainer.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// Get target content ID
const target = tab.getAttribute('data-target');
if (!target) return;
// Remove active class from all tabs
tabs.forEach(t => t.classList.remove('active'));
// Add active class to clicked tab
tab.classList.add('active');
// Hide all tab content
const tabContents = document.querySelectorAll('.tab-content');
tabContents.forEach(content => {
content.classList.remove('active');
});
// Show target content
const targetContent = document.getElementById(target);
if (targetContent) {
targetContent.classList.add('active');
}
});
});
});
}
/**
* Initialize dropdown functionality
*/
function initDropdowns() {
document.querySelectorAll('.dropdown-toggle').forEach(toggle => {
toggle.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const dropdown = toggle.closest('.dropdown');
const menu = dropdown.querySelector('.dropdown-menu');
// Close all other dropdowns
document.querySelectorAll('.dropdown-menu.show').forEach(openMenu => {
if (openMenu !== menu) {
openMenu.classList.remove('show');
}
});
// Toggle this dropdown
menu.classList.toggle('show');
});
});
// Close dropdowns when clicking outside
document.addEventListener('click', (event) => {
if (!event.target.closest('.dropdown')) {
document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
menu.classList.remove('show');
});
}
});
}
/**
* Load dashboard data
*/
function loadDashboardData() {
// Load guilds for server select
loadGuilds();
// Load global settings
loadGlobalSettings();
}
/**
* Load guilds for server select dropdown
*/
function loadGuilds() {
const guildSelect = document.getElementById('guild-select');
if (!guildSelect) return;
// Show loading state
guildSelect.disabled = true;
guildSelect.innerHTML = '<option value="">Loading servers...</option>';
// Fetch guilds from API
API.get('/dashboard/api/guilds')
.then(guilds => {
// Clear loading state
guildSelect.innerHTML = '<option value="">--Please choose a server--</option>';
// Add guilds to select
guilds.forEach(guild => {
const option = document.createElement('option');
option.value = guild.id;
option.textContent = guild.name;
guildSelect.appendChild(option);
});
// Enable select
guildSelect.disabled = false;
// Add change event
guildSelect.addEventListener('change', () => {
const guildId = guildSelect.value;
if (guildId) {
loadGuildSettings(guildId);
} else {
// Hide settings form if no guild selected
const settingsForm = document.getElementById('settings-form');
if (settingsForm) {
settingsForm.style.display = 'none';
}
}
});
})
.catch(error => {
console.error('Error loading guilds:', error);
guildSelect.innerHTML = '<option value="">Error loading servers</option>';
guildSelect.disabled = false;
Toast.error('Failed to load servers. Please try again.');
});
}
/**
* Load settings for a specific guild
* @param {string} guildId - The guild ID
*/
function loadGuildSettings(guildId) {
const settingsForm = document.getElementById('settings-form');
if (!settingsForm) return;
// Show loading state
const loadingContainer = document.createElement('div');
loadingContainer.className = 'loading-container';
loadingContainer.innerHTML = '<div class="loading-spinner"></div><p>Loading server settings...</p>';
loadingContainer.style.textAlign = 'center';
loadingContainer.style.padding = '2rem';
settingsForm.style.display = 'none';
settingsForm.parentNode.insertBefore(loadingContainer, settingsForm);
// Fetch guild settings from API
API.get(`/dashboard/api/guilds/${guildId}/settings`)
.then(settings => {
// Remove loading container
loadingContainer.remove();
// Show settings form
settingsForm.style.display = 'block';
// Populate form with settings
populateGuildSettings(settings);
// Load additional data
loadGuildChannels(guildId);
loadGuildRoles(guildId);
loadGuildCommands(guildId);
})
.catch(error => {
console.error('Error loading guild settings:', error);
loadingContainer.innerHTML = '<p class="text-danger">Error loading server settings. Please try again.</p>';
Toast.error('Failed to load server settings. Please try again.');
});
}
/**
* Populate guild settings form with data
* @param {Object} settings - The guild settings
*/
function populateGuildSettings(settings) {
// Prefix
const prefixInput = document.getElementById('prefix-input');
if (prefixInput) {
prefixInput.value = settings.prefix || '!';
}
// Welcome settings
const welcomeChannel = document.getElementById('welcome-channel');
const welcomeMessage = document.getElementById('welcome-message');
if (welcomeChannel && welcomeMessage) {
welcomeChannel.value = settings.welcome_channel_id || '';
welcomeMessage.value = settings.welcome_message || '';
}
// Goodbye settings
const goodbyeChannel = document.getElementById('goodbye-channel');
const goodbyeMessage = document.getElementById('goodbye-message');
if (goodbyeChannel && goodbyeMessage) {
goodbyeChannel.value = settings.goodbye_channel_id || '';
goodbyeMessage.value = settings.goodbye_message || '';
}
// Cogs (modules)
const cogsList = document.getElementById('cogs-list');
if (cogsList && settings.enabled_cogs) {
cogsList.innerHTML = '';
Object.entries(settings.enabled_cogs).forEach(([cogName, enabled]) => {
const cogDiv = document.createElement('div');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `cog-${cogName}`;
checkbox.name = `cog-${cogName}`;
checkbox.checked = enabled;
const label = document.createElement('label');
label.htmlFor = `cog-${cogName}`;
label.textContent = cogName;
cogDiv.appendChild(checkbox);
cogDiv.appendChild(label);
cogsList.appendChild(cogDiv);
});
}
}
/**
* Load channels for a guild
* @param {string} guildId - The guild ID
*/
function loadGuildChannels(guildId) {
const welcomeChannelSelect = document.getElementById('welcome-channel-select');
const goodbyeChannelSelect = document.getElementById('goodbye-channel-select');
if (!welcomeChannelSelect && !goodbyeChannelSelect) return;
// Fetch channels from API
API.get(`/dashboard/api/guilds/${guildId}/channels`)
.then(channels => {
// Filter text channels
const textChannels = channels.filter(channel => channel.type === 0);
// Populate welcome channel select
if (welcomeChannelSelect) {
welcomeChannelSelect.innerHTML = '<option value="">-- Select Channel --</option>';
textChannels.forEach(channel => {
const option = document.createElement('option');
option.value = channel.id;
option.textContent = `#${channel.name}`;
welcomeChannelSelect.appendChild(option);
});
// Set current value if available
const welcomeChannelInput = document.getElementById('welcome-channel');
if (welcomeChannelInput && welcomeChannelInput.value) {
welcomeChannelSelect.value = welcomeChannelInput.value;
}
// Add change event
welcomeChannelSelect.addEventListener('change', () => {
if (welcomeChannelInput) {
welcomeChannelInput.value = welcomeChannelSelect.value;
}
});
}
// Populate goodbye channel select
if (goodbyeChannelSelect) {
goodbyeChannelSelect.innerHTML = '<option value="">-- Select Channel --</option>';
textChannels.forEach(channel => {
const option = document.createElement('option');
option.value = channel.id;
option.textContent = `#${channel.name}`;
goodbyeChannelSelect.appendChild(option);
});
// Set current value if available
const goodbyeChannelInput = document.getElementById('goodbye-channel');
if (goodbyeChannelInput && goodbyeChannelInput.value) {
goodbyeChannelSelect.value = goodbyeChannelInput.value;
}
// Add change event
goodbyeChannelSelect.addEventListener('change', () => {
if (goodbyeChannelInput) {
goodbyeChannelInput.value = goodbyeChannelSelect.value;
}
});
}
})
.catch(error => {
console.error('Error loading channels:', error);
Toast.error('Failed to load channels. Please try again.');
});
}
/**
* Load roles for a guild
* @param {string} guildId - The guild ID
*/
function loadGuildRoles(guildId) {
const roleSelect = document.getElementById('role-select');
if (!roleSelect) return;
// Fetch roles from API
API.get(`/dashboard/api/guilds/${guildId}/roles`)
.then(roles => {
roleSelect.innerHTML = '<option value="">-- Select Role --</option>';
roles.forEach(role => {
const option = document.createElement('option');
option.value = role.id;
option.textContent = role.name;
// Set color if available
if (role.color) {
option.style.color = `#${role.color.toString(16).padStart(6, '0')}`;
}
roleSelect.appendChild(option);
});
})
.catch(error => {
console.error('Error loading roles:', error);
Toast.error('Failed to load roles. Please try again.');
});
}
/**
* Load commands for a guild
* @param {string} guildId - The guild ID
*/
function loadGuildCommands(guildId) {
const commandSelect = document.getElementById('command-select');
if (!commandSelect) return;
// Fetch commands from API
API.get(`/dashboard/api/guilds/${guildId}/commands`)
.then(commands => {
commandSelect.innerHTML = '<option value="">-- Select Command --</option>';
commands.forEach(command => {
const option = document.createElement('option');
option.value = command.name;
option.textContent = command.name;
// Add description as title attribute
if (command.description) {
option.title = command.description;
}
commandSelect.appendChild(option);
});
// Load command permissions
loadCommandPermissions(guildId);
})
.catch(error => {
console.error('Error loading commands:', error);
Toast.error('Failed to load commands. Please try again.');
});
}
/**
* Load command permissions for a guild
* @param {string} guildId - The guild ID
*/
function loadCommandPermissions(guildId) {
const currentPerms = document.getElementById('current-perms');
if (!currentPerms) return;
// Show loading state
currentPerms.innerHTML = '<div class="loading-spinner-container"><div class="loading-spinner loading-spinner-sm"></div></div>';
// Fetch command permissions from API
API.get(`/dashboard/api/guilds/${guildId}/command-permissions`)
.then(permissions => {
// Clear loading state
currentPerms.innerHTML = '';
if (permissions.length === 0) {
currentPerms.innerHTML = '<div class="text-gray">No custom permissions set.</div>';
return;
}
// Group permissions by command
const permsByCommand = {};
permissions.forEach(perm => {
if (!permsByCommand[perm.command]) {
permsByCommand[perm.command] = [];
}
permsByCommand[perm.command].push(perm);
});
// Create permission elements
Object.entries(permsByCommand).forEach(([command, perms]) => {
const commandDiv = document.createElement('div');
commandDiv.className = 'command-perms';
commandDiv.innerHTML = `<div class="command-name">${command}</div>`;
const rolesList = document.createElement('div');
rolesList.className = 'roles-list';
perms.forEach(perm => {
const roleSpan = document.createElement('span');
roleSpan.className = 'role-badge';
roleSpan.textContent = perm.role_name;
roleSpan.dataset.roleId = perm.role_id;
roleSpan.dataset.command = command;
// Add remove button
const removeBtn = document.createElement('button');
removeBtn.className = 'role-remove';
removeBtn.innerHTML = '&times;';
removeBtn.addEventListener('click', () => {
removeCommandPermission(guildId, command, perm.role_id);
});
roleSpan.appendChild(removeBtn);
rolesList.appendChild(roleSpan);
});
commandDiv.appendChild(rolesList);
currentPerms.appendChild(commandDiv);
});
})
.catch(error => {
console.error('Error loading command permissions:', error);
currentPerms.innerHTML = '<div class="text-danger">Error loading permissions.</div>';
});
}
/**
* Add a command permission
* @param {string} guildId - The guild ID
* @param {string} command - The command name
* @param {string} roleId - The role ID
*/
function addCommandPermission(guildId, command, roleId) {
const addPermButton = document.getElementById('add-perm-button');
const permsFeedback = document.getElementById('perms-feedback');
if (!command || !roleId) {
permsFeedback.textContent = 'Please select both a command and a role.';
permsFeedback.className = 'error';
return;
}
// Show loading state
addPermButton.disabled = true;
addPermButton.classList.add('btn-loading');
// Send request to API
API.post(`/dashboard/api/guilds/${guildId}/command-permissions`, {
command,
role_id: roleId
})
.then(() => {
// Show success message
permsFeedback.textContent = 'Permission added successfully.';
permsFeedback.className = '';
// Reload permissions
loadCommandPermissions(guildId);
// Reset form
document.getElementById('command-select').value = '';
document.getElementById('role-select').value = '';
})
.catch(error => {
console.error('Error adding permission:', error);
permsFeedback.textContent = 'Error adding permission. Please try again.';
permsFeedback.className = 'error';
})
.finally(() => {
// Remove loading state
addPermButton.disabled = false;
addPermButton.classList.remove('btn-loading');
});
}
/**
* Remove a command permission
* @param {string} guildId - The guild ID
* @param {string} command - The command name
* @param {string} roleId - The role ID
*/
function removeCommandPermission(guildId, command, roleId) {
// Send request to API
API.delete(`/dashboard/api/guilds/${guildId}/command-permissions?command=${command}&role_id=${roleId}`)
.then(() => {
// Show success message
Toast.success('Permission removed successfully.');
// Reload permissions
loadCommandPermissions(guildId);
})
.catch(error => {
console.error('Error removing permission:', error);
Toast.error('Error removing permission. Please try again.');
});
}
/**
* Load global AI settings
*/
function loadGlobalSettings() {
const aiSettingsSection = document.getElementById('ai-settings-section');
if (!aiSettingsSection) return;
// Fetch global settings from API
API.get('/dashboard/api/settings')
.then(settings => {
// Populate AI model select
const modelSelect = document.getElementById('ai-model-select');
if (modelSelect && settings.model) {
modelSelect.value = settings.model;
}
// Populate temperature slider
const temperatureSlider = document.getElementById('ai-temperature');
const temperatureValue = document.getElementById('temperature-value');
if (temperatureSlider && temperatureValue && settings.temperature) {
temperatureSlider.value = settings.temperature;
temperatureValue.textContent = settings.temperature;
// Add input event for live update
temperatureSlider.addEventListener('input', () => {
temperatureValue.textContent = temperatureSlider.value;
});
}
// Populate max tokens
const maxTokensInput = document.getElementById('ai-max-tokens');
if (maxTokensInput && settings.max_tokens) {
maxTokensInput.value = settings.max_tokens;
}
// Populate character settings
const characterInput = document.getElementById('ai-character');
const characterInfoInput = document.getElementById('ai-character-info');
if (characterInput && settings.character) {
characterInput.value = settings.character;
}
if (characterInfoInput && settings.character_info) {
characterInfoInput.value = settings.character_info;
}
// Populate system prompt
const systemPromptInput = document.getElementById('ai-system-prompt');
if (systemPromptInput && settings.system_message) {
systemPromptInput.value = settings.system_message;
}
// Populate custom instructions
const customInstructionsInput = document.getElementById('ai-custom-instructions');
if (customInstructionsInput && settings.custom_instructions) {
customInstructionsInput.value = settings.custom_instructions;
}
})
.catch(error => {
console.error('Error loading global settings:', error);
Toast.error('Failed to load AI settings. Please try again.');
});
}

View File

@ -0,0 +1,537 @@
/**
* Utility functions for the Discord Bot Dashboard
*/
// Toast notification system
const Toast = {
container: null,
/**
* Initialize the toast container
*/
init() {
// Create toast container if it doesn't exist
if (!this.container) {
this.container = document.createElement('div');
this.container.className = 'toast-container';
document.body.appendChild(this.container);
}
},
/**
* Show a toast notification
* @param {string} message - The message to display
* @param {string} type - The type of toast (success, error, warning, info)
* @param {string} title - Optional title for the toast
* @param {number} duration - Duration in milliseconds before auto-hiding
*/
show(message, type = 'info', title = '', duration = 5000) {
this.init();
// Create toast element
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
// Create icon based on type
let iconSvg = '';
switch (type) {
case 'success':
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#48BB78" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>';
break;
case 'error':
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F56565" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>';
break;
case 'warning':
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F6AD55" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>';
break;
default: // info
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#5865F2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>';
}
// Set toast content
toast.innerHTML = `
<div class="toast-icon">${iconSvg}</div>
<div class="toast-content">
${title ? `<div class="toast-title">${title}</div>` : ''}
<div class="toast-message">${message}</div>
</div>
<button class="toast-close">&times;</button>
`;
// Add to container
this.container.appendChild(toast);
// Add close event
const closeBtn = toast.querySelector('.toast-close');
closeBtn.addEventListener('click', () => this.hide(toast));
// Auto-hide after duration
if (duration) {
setTimeout(() => this.hide(toast), duration);
}
return toast;
},
/**
* Hide a toast notification
* @param {HTMLElement} toast - The toast element to hide
*/
hide(toast) {
toast.classList.add('toast-hiding');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300); // Match animation duration
},
/**
* Show a success toast
* @param {string} message - The message to display
* @param {string} title - Optional title
*/
success(message, title = 'Success') {
return this.show(message, 'success', title);
},
/**
* Show an error toast
* @param {string} message - The message to display
* @param {string} title - Optional title
*/
error(message, title = 'Error') {
return this.show(message, 'error', title);
},
/**
* Show a warning toast
* @param {string} message - The message to display
* @param {string} title - Optional title
*/
warning(message, title = 'Warning') {
return this.show(message, 'warning', title);
},
/**
* Show an info toast
* @param {string} message - The message to display
* @param {string} title - Optional title
*/
info(message, title = 'Info') {
return this.show(message, 'info', title);
}
};
// API utilities
const API = {
/**
* Make an API request with loading state
* @param {string} url - The API endpoint URL
* @param {Object} options - Fetch options
* @param {HTMLElement} loadingElement - Element to show loading state on
* @returns {Promise} - The fetch promise
*/
async request(url, options = {}, loadingElement = null) {
// Set default headers
options.headers = options.headers || {};
options.headers['Content-Type'] = options.headers['Content-Type'] || 'application/json';
// Add loading state
if (loadingElement) {
if (loadingElement.tagName === 'BUTTON') {
loadingElement.disabled = true;
loadingElement.classList.add('btn-loading');
} else {
// Create or use existing loading overlay
let overlay = loadingElement.querySelector('.loading-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.innerHTML = '<div class="loading-spinner"></div>';
// Make sure the element has position relative for absolute positioning
const computedStyle = window.getComputedStyle(loadingElement);
if (computedStyle.position === 'static') {
loadingElement.style.position = 'relative';
}
loadingElement.appendChild(overlay);
} else {
overlay.style.display = 'flex';
}
}
}
try {
const response = await fetch(url, options);
// Parse JSON response
let data;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
// Remove loading state
if (loadingElement) {
if (loadingElement.tagName === 'BUTTON') {
loadingElement.disabled = false;
loadingElement.classList.remove('btn-loading');
} else {
const overlay = loadingElement.querySelector('.loading-overlay');
if (overlay) {
overlay.style.display = 'none';
}
}
}
// Handle error responses
if (!response.ok) {
const error = new Error(data.message || data.error || 'API request failed');
error.status = response.status;
error.data = data;
throw error;
}
return data;
} catch (error) {
// Remove loading state
if (loadingElement) {
if (loadingElement.tagName === 'BUTTON') {
loadingElement.disabled = false;
loadingElement.classList.remove('btn-loading');
} else {
const overlay = loadingElement.querySelector('.loading-overlay');
if (overlay) {
overlay.style.display = 'none';
}
}
}
// Show error toast
Toast.error(error.message || 'An error occurred');
throw error;
}
},
/**
* Make a GET request
* @param {string} url - The API endpoint URL
* @param {HTMLElement} loadingElement - Element to show loading state on
* @returns {Promise} - The fetch promise
*/
async get(url, loadingElement = null) {
return this.request(url, { method: 'GET' }, loadingElement);
},
/**
* Make a POST request
* @param {string} url - The API endpoint URL
* @param {Object} data - The data to send
* @param {HTMLElement} loadingElement - Element to show loading state on
* @returns {Promise} - The fetch promise
*/
async post(url, data, loadingElement = null) {
return this.request(
url,
{
method: 'POST',
body: JSON.stringify(data)
},
loadingElement
);
},
/**
* Make a PUT request
* @param {string} url - The API endpoint URL
* @param {Object} data - The data to send
* @param {HTMLElement} loadingElement - Element to show loading state on
* @returns {Promise} - The fetch promise
*/
async put(url, data, loadingElement = null) {
return this.request(
url,
{
method: 'PUT',
body: JSON.stringify(data)
},
loadingElement
);
},
/**
* Make a DELETE request
* @param {string} url - The API endpoint URL
* @param {HTMLElement} loadingElement - Element to show loading state on
* @returns {Promise} - The fetch promise
*/
async delete(url, loadingElement = null) {
return this.request(url, { method: 'DELETE' }, loadingElement);
}
};
// Modal utilities
const Modal = {
/**
* Open a modal
* @param {string} modalId - The ID of the modal to open
*/
open(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('active');
document.body.style.overflow = 'hidden'; // Prevent scrolling
}
},
/**
* Close a modal
* @param {string} modalId - The ID of the modal to close
*/
close(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('active');
document.body.style.overflow = ''; // Restore scrolling
}
},
/**
* Initialize modal close buttons
*/
init() {
// Close modal when clicking the close button
document.querySelectorAll('.modal-close').forEach(button => {
button.addEventListener('click', () => {
const modal = button.closest('.modal');
if (modal) {
modal.classList.remove('active');
document.body.style.overflow = ''; // Restore scrolling
}
});
});
// Close modal when clicking outside the modal content
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (event) => {
if (event.target === modal) {
modal.classList.remove('active');
document.body.style.overflow = ''; // Restore scrolling
}
});
});
}
};
// Form utilities
const Form = {
/**
* Serialize form data to an object
* @param {HTMLFormElement} form - The form element
* @returns {Object} - The serialized form data
*/
serialize(form) {
const formData = new FormData(form);
const data = {};
for (const [key, value] of formData.entries()) {
// Handle checkboxes
if (form.elements[key].type === 'checkbox') {
data[key] = value === 'on';
} else {
data[key] = value;
}
}
return data;
},
/**
* Populate a form with data
* @param {HTMLFormElement} form - The form element
* @param {Object} data - The data to populate the form with
*/
populate(form, data) {
for (const key in data) {
if (form.elements[key]) {
const element = form.elements[key];
if (element.type === 'checkbox') {
element.checked = Boolean(data[key]);
} else if (element.type === 'radio') {
const radio = form.querySelector(`input[name="${key}"][value="${data[key]}"]`);
if (radio) {
radio.checked = true;
}
} else {
element.value = data[key];
}
// Trigger change event for elements like select
const event = new Event('change');
element.dispatchEvent(event);
}
}
},
/**
* Validate a form
* @param {HTMLFormElement} form - The form element
* @returns {boolean} - Whether the form is valid
*/
validate(form) {
let isValid = true;
// Remove existing error messages
form.querySelectorAll('.form-error').forEach(error => {
error.remove();
});
// Check required fields
form.querySelectorAll('[required]').forEach(field => {
if (!field.value.trim()) {
isValid = false;
this.showError(field, 'This field is required');
}
});
// Check email fields
form.querySelectorAll('input[type="email"]').forEach(field => {
if (field.value && !this.isValidEmail(field.value)) {
isValid = false;
this.showError(field, 'Please enter a valid email address');
}
});
return isValid;
},
/**
* Show an error message for a form field
* @param {HTMLElement} field - The form field
* @param {string} message - The error message
*/
showError(field, message) {
// Create error message element
const error = document.createElement('div');
error.className = 'form-error';
error.textContent = message;
error.style.color = 'var(--danger-color)';
error.style.fontSize = '0.875rem';
error.style.marginTop = '0.25rem';
// Add error class to field
field.classList.add('is-invalid');
// Insert error after field
field.parentNode.insertBefore(error, field.nextSibling);
// Add event listener to remove error when field is changed
field.addEventListener('input', () => {
field.classList.remove('is-invalid');
if (error.parentNode) {
error.parentNode.removeChild(error);
}
}, { once: true });
},
/**
* Check if an email is valid
* @param {string} email - The email to check
* @returns {boolean} - Whether the email is valid
*/
isValidEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
};
// DOM utilities
const DOM = {
/**
* Create an element with attributes and children
* @param {string} tag - The tag name
* @param {Object} attrs - The attributes
* @param {Array|string} children - The children
* @returns {HTMLElement} - The created element
*/
createElement(tag, attrs = {}, children = []) {
const element = document.createElement(tag);
// Set attributes
for (const key in attrs) {
if (key === 'className') {
element.className = attrs[key];
} else if (key === 'style' && typeof attrs[key] === 'object') {
Object.assign(element.style, attrs[key]);
} else if (key.startsWith('on') && typeof attrs[key] === 'function') {
const eventName = key.substring(2).toLowerCase();
element.addEventListener(eventName, attrs[key]);
} else {
element.setAttribute(key, attrs[key]);
}
}
// Add children
if (Array.isArray(children)) {
children.forEach(child => {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else if (child instanceof Node) {
element.appendChild(child);
}
});
} else if (typeof children === 'string') {
element.textContent = children;
}
return element;
},
/**
* Create a loading spinner
* @param {string} size - The size of the spinner (sm, md, lg)
* @returns {HTMLElement} - The spinner element
*/
createSpinner(size = 'md') {
const spinner = document.createElement('div');
spinner.className = `loading-spinner loading-spinner-${size}`;
return spinner;
},
/**
* Show a loading spinner in a container
* @param {HTMLElement} container - The container element
* @param {string} size - The size of the spinner
* @returns {HTMLElement} - The spinner container element
*/
showSpinner(container, size = 'md') {
// Clear container
container.innerHTML = '';
// Create spinner container
const spinnerContainer = document.createElement('div');
spinnerContainer.className = 'loading-spinner-container';
// Create spinner
const spinner = this.createSpinner(size);
spinnerContainer.appendChild(spinner);
// Add to container
container.appendChild(spinnerContainer);
return spinnerContainer;
}
};
// Export utilities
window.Toast = Toast;
window.API = API;
window.Modal = Modal;
window.Form = Form;
window.DOM = DOM;

View File

@ -1,9 +1,22 @@
// This file is kept for backward compatibility
// It will load the new modular JS files
// Load the utility functions
const utilsScript = document.createElement('script');
utilsScript.src = 'js/utils.js';
document.head.appendChild(utilsScript);
// Load the main script
const mainScript = document.createElement('script');
mainScript.src = 'js/main.js';
document.head.appendChild(mainScript);
document.addEventListener('DOMContentLoaded', () => {
// Auth elements
const loginButton = document.getElementById('login-button');
const logoutButton = document.getElementById('logout-button');
const authSection = document.getElementById('auth-section');
const dashboardSection = document.getElementById('dashboard-section');
const dashboardSection = document.getElementById('dashboard-container');
const usernameSpan = document.getElementById('username');
// Navigation elements

View File

@ -1,11 +1,20 @@
/* This file is kept for backward compatibility */
/* It imports the new modular CSS files */
@import url('css/main.css');
@import url('css/components.css');
@import url('css/layout.css');
/* Legacy styles below for backward compatibility */
body {
font-family: sans-serif;
margin: 2em;
background-color: #f4f4f4;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
margin: 0;
background-color: var(--light-bg);
color: var(--text-primary);
}
h1, h2, h3, h4 {
color: #333;
color: var(--secondary-color);
}
#dashboard-section {