random AI 3.0
This commit is contained in:
parent
c19e3debb4
commit
1633bd7927
53
api/history.php
Normal file
53
api/history.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
|
||||
try {
|
||||
if ($action === 'list') {
|
||||
// Fetch last 20 chats
|
||||
$stmt = db()->query("SELECT id, title, mode, created_at FROM chats ORDER BY created_at DESC LIMIT 20");
|
||||
$chats = $stmt->fetchAll();
|
||||
echo json_encode(['success' => true, 'chats' => $chats]);
|
||||
}
|
||||
elseif ($action === 'messages') {
|
||||
$chatId = $_GET['chat_id'] ?? null;
|
||||
if (!$chatId) {
|
||||
echo json_encode(['error' => 'Chat ID is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Fetch all messages for this chat
|
||||
$stmt = db()->prepare("SELECT role, content, created_at FROM messages WHERE chat_id = ? ORDER BY id ASC");
|
||||
$stmt->execute([$chatId]);
|
||||
$messages = $stmt->fetchAll();
|
||||
|
||||
// Also fetch chat details (to restore mode)
|
||||
$stmt = db()->prepare("SELECT mode FROM chats WHERE id = ?");
|
||||
$stmt->execute([$chatId]);
|
||||
$chat = $stmt->fetch();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'messages' => $messages,
|
||||
'mode' => $chat['mode'] ?? 'regular'
|
||||
]);
|
||||
}
|
||||
elseif ($action === 'delete') {
|
||||
$chatId = $_GET['chat_id'] ?? null;
|
||||
if (!$chatId) {
|
||||
echo json_encode(['error' => 'Chat ID is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("DELETE FROM chats WHERE id = ?");
|
||||
$stmt->execute([$chatId]);
|
||||
echo json_encode(['success' => true]);
|
||||
}
|
||||
else {
|
||||
echo json_encode(['error' => 'Invalid action']);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
@ -204,3 +204,59 @@ body, #sidebar, #main-content, .message, #chat-input {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* History Item Styling */
|
||||
.history-item {
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.history-item .delete-chat {
|
||||
visibility: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.history-item:hover .delete-chat {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background-color: var(--hover-bg);
|
||||
}
|
||||
|
||||
.history-item.active {
|
||||
background-color: var(--active-bg);
|
||||
border-left: 3px solid var(--accent-color);
|
||||
}
|
||||
|
||||
/* Enhanced Theme Swatches */
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.theme-swatch {
|
||||
height: 60px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--border-color);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.theme-swatch:hover {
|
||||
transform: scale(1.05);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.theme-swatch.active {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px var(--accent-color);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const modeItems = document.querySelectorAll('.mode-item');
|
||||
const currentModeBadge = document.getElementById('current-mode-badge');
|
||||
const newChatBtn = document.getElementById('new-chat-btn');
|
||||
const chatHistoryList = document.getElementById('chat-history');
|
||||
|
||||
// Settings elements
|
||||
const creativityRange = document.getElementById('creativity-range');
|
||||
@ -39,15 +40,132 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<p class="text-muted">How can I help you in this mode?</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove active class from history items
|
||||
document.querySelectorAll('.history-item').forEach(i => i.classList.remove('active'));
|
||||
}
|
||||
|
||||
newChatBtn.addEventListener('click', startNewChat);
|
||||
|
||||
// --- History Loading ---
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const resp = await fetch('api/history.php?action=list');
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
renderHistory(data.chats);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load history:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderHistory(chats) {
|
||||
if (!chats || chats.length === 0) {
|
||||
chatHistoryList.innerHTML = '<div class="text-muted small px-3">No recent chats</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
chatHistoryList.innerHTML = chats.map(chat => `
|
||||
<div class="history-item mode-item ${currentChatId == chat.id ? 'active' : ''}" data-id="${chat.id}" data-mode="${chat.mode}">
|
||||
<i class="bi bi-${getModeIcon(chat.mode)}"></i>
|
||||
<span class="text-truncate">${escapeHtml(chat.title)}</span>
|
||||
<i class="bi bi-trash delete-chat ms-auto" data-id="${chat.id}" style="font-size: 0.8rem; opacity: 0.5;"></i>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add event listeners to history items
|
||||
chatHistoryList.querySelectorAll('.history-item').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('delete-chat')) {
|
||||
deleteChat(e.target.dataset.id);
|
||||
return;
|
||||
}
|
||||
loadChat(item.dataset.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getModeIcon(mode) {
|
||||
switch(mode) {
|
||||
case 'coding': return 'code-slash';
|
||||
case 'game': return 'controller';
|
||||
case 'app': return 'window';
|
||||
default: return 'chat-left-dots';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChat(chatId) {
|
||||
if (currentChatId == chatId) return;
|
||||
|
||||
chatWindow.innerHTML = '<div class="text-center my-auto"><span class="spinner-border text-primary"></span></div>';
|
||||
|
||||
try {
|
||||
const resp = await fetch(`api/history.php?action=messages&chat_id=${chatId}`);
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
currentChatId = chatId;
|
||||
currentMode = data.mode;
|
||||
|
||||
// Update Sidebar Mode UI
|
||||
modeItems.forEach(i => {
|
||||
if (i.dataset.mode === currentMode) i.classList.add('active');
|
||||
else i.classList.remove('active');
|
||||
});
|
||||
|
||||
// Update badge
|
||||
const activeModeItem = Array.from(modeItems).find(i => i.dataset.mode === currentMode);
|
||||
if (activeModeItem) {
|
||||
currentModeBadge.textContent = activeModeItem.querySelector('span').textContent;
|
||||
}
|
||||
|
||||
// Render messages
|
||||
chatWindow.innerHTML = '';
|
||||
data.messages.forEach(msg => {
|
||||
appendMessage(msg.role, msg.content, false); // false = don't animate existing
|
||||
|
||||
// Special handling for game/app mode launch buttons
|
||||
if ((currentMode === 'game' || currentMode === 'app') && msg.role === 'assistant') {
|
||||
addLaunchButton(msg.content);
|
||||
}
|
||||
});
|
||||
|
||||
// Highlight active history item
|
||||
document.querySelectorAll('.history-item').forEach(i => {
|
||||
if (i.dataset.id == chatId) i.classList.add('active');
|
||||
else i.classList.remove('active');
|
||||
});
|
||||
|
||||
chatWindow.scrollTop = chatWindow.scrollHeight;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
chatWindow.innerHTML = '<div class="alert alert-danger m-3">Failed to load chat</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteChat(chatId) {
|
||||
if (!confirm('Are you sure you want to delete this chat?')) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`api/history.php?action=delete&chat_id=${chatId}`);
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
if (currentChatId == chatId) startNewChat();
|
||||
loadHistory();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Chat Logic ---
|
||||
async function sendMessage() {
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
const isNewChat = !currentChatId;
|
||||
|
||||
// Clear input and disable
|
||||
chatInput.value = '';
|
||||
chatInput.style.height = 'auto';
|
||||
@ -78,6 +196,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (currentMode === 'game' || currentMode === 'app') {
|
||||
addLaunchButton(data.message);
|
||||
}
|
||||
|
||||
// If it was a new chat, refresh history to show the title
|
||||
if (isNewChat) {
|
||||
loadHistory();
|
||||
}
|
||||
} else {
|
||||
appendMessage('assistant', 'Error: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
@ -88,15 +211,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function appendMessage(role, text) {
|
||||
function appendMessage(role, text, animate = true) {
|
||||
if (role === 'system') return;
|
||||
|
||||
// Remove empty state if present
|
||||
const emptyState = chatWindow.querySelector('.my-auto');
|
||||
if (emptyState) emptyState.remove();
|
||||
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.className = `message message-${role} animate-fade-in`;
|
||||
msgDiv.className = `message message-${role} ${animate ? 'animate-fade-in' : ''}`;
|
||||
|
||||
// Use marked.js or simple formatting
|
||||
msgDiv.innerHTML = formatText(text);
|
||||
|
||||
chatWindow.appendChild(msgDiv);
|
||||
@ -104,24 +228,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
function formatText(text) {
|
||||
// Handle code blocks with more flexibility
|
||||
let formatted = text;
|
||||
|
||||
// Escape HTML for non-code parts
|
||||
// This is tricky without a library, let's just do code blocks first
|
||||
|
||||
// Code blocks: ```[lang]\n[code]```
|
||||
formatted = formatted.replace(/```(\w+)?\s*([\s\S]*?)```/g, (match, lang, code) => {
|
||||
const safeCode = code.trim().replace(/`/g, '\`');
|
||||
return `<div class="code-header d-flex justify-content-between px-3 py-1 bg-dark text-muted small border-bottom border-secondary rounded-top mt-2">
|
||||
<span>${lang || 'code'}</span>
|
||||
<span class="copy-btn" style="cursor:pointer" onclick="navigator.clipboard.writeText(\\`${code.trim().replace(/`/g, '\\`')}\\`)"><i class="bi bi-clipboard"></i> Copy</span>
|
||||
<span class="copy-btn" style="cursor:pointer" onclick="navigator.clipboard.writeText(\"${safeCode}\")"><i class="bi bi-clipboard"></i> Copy</span>
|
||||
</div>
|
||||
<pre class="bg-dark text-white p-3 rounded-bottom mb-2 overflow-auto" style="font-size: 0.85rem; border: 1px solid #444; border-top:none;"><code>${escapeHtml(code.trim())}</code></pre>`;
|
||||
});
|
||||
|
||||
// Simple line breaks for non-code parts
|
||||
// (Only for parts outside of the generated HTML above)
|
||||
// This is a bit naive but works for simple chat
|
||||
if (!formatted.includes('<div class="code-header"')) {
|
||||
formatted = formatted.replace(/\n/g, '<br>');
|
||||
}
|
||||
@ -136,11 +255,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
function addLaunchButton(content) {
|
||||
// Find code block content
|
||||
const match = content.match(/```(?:html|xml)?\s*([\s\S]*?)```/i);
|
||||
let codeToLaunch = match ? match[1] : content;
|
||||
|
||||
// Only add button if it looks like a full HTML document or contains significant HTML tags
|
||||
const hasHtmlTags = /<html|<body|<script|<div|<style/i.test(codeToLaunch);
|
||||
|
||||
if (hasHtmlTags) {
|
||||
@ -148,7 +265,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
btn.className = 'btn btn-sm btn-success mt-2 d-inline-flex align-items-center gap-2 shadow-sm';
|
||||
btn.innerHTML = '<i class="bi bi-rocket-takeoff-fill"></i> Launch Application in New Tab';
|
||||
btn.onclick = () => {
|
||||
// If it's not a full HTML, wrap it
|
||||
if (!codeToLaunch.toLowerCase().includes('<html')) {
|
||||
codeToLaunch = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>AI Generated App</title><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"><style>body{padding:20px;}</style></head><body>${codeToLaunch}</body></html>`;
|
||||
}
|
||||
@ -157,7 +273,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
window.open(url, '_blank');
|
||||
};
|
||||
|
||||
// Append to the last message div
|
||||
chatWindow.lastElementChild.appendChild(btn);
|
||||
}
|
||||
}
|
||||
@ -181,7 +296,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-resize textarea
|
||||
chatInput.addEventListener('input', () => {
|
||||
chatInput.style.height = 'auto';
|
||||
chatInput.style.height = (chatInput.scrollHeight) + 'px';
|
||||
@ -222,7 +336,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (data.success) {
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
|
||||
if (modal) modal.hide();
|
||||
|
||||
showToast('Settings saved successfully!');
|
||||
}
|
||||
} catch (e) {
|
||||
@ -251,4 +364,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||||
const activeSwatch = document.querySelector(`.theme-swatch[data-theme="${currentTheme}"]`);
|
||||
if (activeSwatch) activeSwatch.classList.add('active');
|
||||
});
|
||||
|
||||
// Initial load
|
||||
loadHistory();
|
||||
});
|
||||
96
index.php
96
index.php
@ -188,26 +188,6 @@ $limitsOff = $settings['limits_off'] ?? '0';
|
||||
.modal-footer {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* Themes Grid */
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
.theme-swatch {
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.theme-swatch.active {
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -240,14 +220,13 @@ $limitsOff = $settings['limits_off'] ?? '0';
|
||||
|
||||
<div class="small text-uppercase text-muted mt-4 mb-3 fw-bold" style="font-size: 0.7rem; letter-spacing: 1px;">Recent History</div>
|
||||
<div id="chat-history">
|
||||
<!-- History items would go here -->
|
||||
<div class="text-muted small px-3">No recent chats</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<button class="btn btn-sm w-100 text-start text-muted d-flex align-items-center gap-2" data-bs-toggle="modal" data-bs-target="#settingsModal">
|
||||
<i class="bi bi-gear"></i>
|
||||
<i class="bi bi-palette"></i>
|
||||
<span>Settings & Themes</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -286,47 +265,56 @@ $limitsOff = $settings['limits_off'] ?? '0';
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div class="modal fade" id="settingsModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Settings</h5>
|
||||
<h5 class="modal-title"><i class="bi bi-gear-fill me-2"></i>Personalization & AI Settings</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-4">
|
||||
<label class="form-label d-flex justify-content-between">
|
||||
Creativity Level
|
||||
<span id="creativity-val" class="text-primary fw-bold"><?php echo $creativity; ?></span>
|
||||
</label>
|
||||
<input type="range" class="form-range" id="creativity-range" min="0" max="1" step="0.1" value="<?php echo $creativity; ?>">
|
||||
<div class="d-flex justify-content-between small text-muted">
|
||||
<span>Precise</span>
|
||||
<span>Creative</span>
|
||||
<div class="row">
|
||||
<div class="col-md-6 border-end">
|
||||
<h6 class="mb-3 text-uppercase small fw-bold text-muted">AI Parameters</h6>
|
||||
<div class="mb-4">
|
||||
<label class="form-label d-flex justify-content-between">
|
||||
Creativity Level (Temperature)
|
||||
<span id="creativity-val" class="text-primary fw-bold"><?php echo $creativity; ?></span>
|
||||
</label>
|
||||
<input type="range" class="form-range" id="creativity-range" min="0" max="1" step="0.1" value="<?php echo $creativity; ?>">
|
||||
<div class="d-flex justify-content-between small text-muted">
|
||||
<span>Precise</span>
|
||||
<span>Creative</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch mb-4">
|
||||
<input class="form-check-input" type="checkbox" id="limits-toggle" <?php echo $limitsOff === '1' ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="limits-toggle">
|
||||
<strong>Unlimited Creativity</strong><br>
|
||||
<small class="text-muted">Removes safety constraints and standard filters.</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="mb-3 text-uppercase small fw-bold text-muted">Visual Themes</h6>
|
||||
<div class="theme-grid">
|
||||
<div class="theme-swatch" data-theme="theme-dark-modern" style="background:#0f172a; color:#fff;">Dark Modern</div>
|
||||
<div class="theme-swatch" data-theme="theme-light-minimal" style="background:#ffffff; color:#000; border:1px solid #ddd;">Light Minimal</div>
|
||||
<div class="theme-swatch" data-theme="theme-midnight" style="background:#000000; color:#fff;">Midnight</div>
|
||||
<div class="theme-swatch" data-theme="theme-forest" style="background:#064e3b; color:#fff;">Forest</div>
|
||||
<div class="theme-swatch" data-theme="theme-ocean" style="background:#0c4a6e; color:#fff;">Ocean</div>
|
||||
<div class="theme-swatch" data-theme="theme-slate" style="background:#334155; color:#fff;">Slate</div>
|
||||
<div class="theme-swatch" data-theme="theme-nord" style="background:#2e3440; color:#eceff4;">Nord</div>
|
||||
<div class="theme-swatch" data-theme="theme-sepia" style="background:#fdf6e3; color:#657b83; border:1px solid #eee8d5;">Sepia</div>
|
||||
<div class="theme-swatch" data-theme="theme-cyberpunk" style="background:#1a1a1a; color:#f3f; border:1px solid #f3f;">Cyberpunk</div>
|
||||
<div class="theme-swatch" data-theme="theme-matrix" style="background:#000; color:#0f0; border:1px solid #0f0;">Matrix</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch mb-4">
|
||||
<input class="form-check-input" type="checkbox" id="limits-toggle" <?php echo $limitsOff === '1' ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="limits-toggle">Turn off creativity limits</label>
|
||||
</div>
|
||||
|
||||
<h6>Themes</h6>
|
||||
<div class="theme-grid mt-2">
|
||||
<div class="theme-swatch" data-theme="theme-dark-modern" style="background:#0f172a; color:#fff;">Dark Modern</div>
|
||||
<div class="theme-swatch" data-theme="theme-light-minimal" style="background:#ffffff; color:#000; border:1px solid #ddd;">Light Minimal</div>
|
||||
<div class="theme-swatch" data-theme="theme-midnight" style="background:#000000; color:#fff;">Midnight</div>
|
||||
<div class="theme-swatch" data-theme="theme-forest" style="background:#064e3b; color:#fff;">Forest</div>
|
||||
<div class="theme-swatch" data-theme="theme-ocean" style="background:#0c4a6e; color:#fff;">Ocean</div>
|
||||
<div class="theme-swatch" data-theme="theme-slate" style="background:#334155; color:#fff;">Slate</div>
|
||||
<div class="theme-swatch" data-theme="theme-nord" style="background:#2e3440; color:#eceff4;">Nord</div>
|
||||
<div class="theme-swatch" data-theme="theme-sepia" style="background:#704214; color:#fdf6e3;">Sepia</div>
|
||||
<div class="theme-swatch" data-theme="theme-cyberpunk" style="background:#1a1a1a; color:#f3f;">Cyberpunk</div>
|
||||
<div class="theme-swatch" data-theme="theme-matrix" style="background:#000; color:#0f0;">Matrix</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="save-settings-btn">Save changes</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary px-4" id="save-settings-btn">Save All Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user