mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-31 04:45:23 +08:00
245 lines
9.1 KiB
HTML
245 lines
9.1 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>FAQ Manager</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: var(--langbot-bg, #fff);
|
|
color: var(--langbot-text, #0a0a0a);
|
|
padding: 24px;
|
|
transition: background 0.2s, color 0.2s;
|
|
}
|
|
.header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
|
|
.header h1 { font-size: 22px; font-weight: 700; }
|
|
.count { font-size: 13px; color: var(--langbot-text-muted, #71717a); }
|
|
|
|
/* Search */
|
|
.search-bar { display: flex; gap: 8px; margin-bottom: 20px; }
|
|
.search-bar input {
|
|
flex: 1; padding: 8px 12px; border: 1px solid var(--langbot-border, #e4e4e7);
|
|
border-radius: 8px; background: var(--langbot-bg, #fff); color: var(--langbot-text, #0a0a0a);
|
|
font-size: 14px; outline: none; transition: border-color 0.15s;
|
|
}
|
|
.search-bar input:focus { border-color: var(--langbot-accent, #2563eb); }
|
|
|
|
/* Form */
|
|
.add-form {
|
|
border: 1px solid var(--langbot-border, #e4e4e7); border-radius: 10px;
|
|
padding: 16px; margin-bottom: 20px; background: var(--langbot-bg-card, #f8fafc);
|
|
}
|
|
.add-form .form-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; }
|
|
.add-form input, .add-form textarea {
|
|
width: 100%; padding: 8px 12px; border: 1px solid var(--langbot-border, #e4e4e7);
|
|
border-radius: 8px; background: var(--langbot-bg, #fff); color: var(--langbot-text, #0a0a0a);
|
|
font-size: 14px; font-family: inherit; outline: none; margin-bottom: 10px; transition: border-color 0.15s;
|
|
}
|
|
.add-form input:focus, .add-form textarea:focus { border-color: var(--langbot-accent, #2563eb); }
|
|
.add-form textarea { resize: vertical; min-height: 60px; }
|
|
|
|
/* Buttons */
|
|
.btn {
|
|
padding: 7px 16px; border: none; border-radius: 8px; font-size: 13px; font-weight: 500;
|
|
cursor: pointer; transition: opacity 0.15s;
|
|
}
|
|
.btn:hover { opacity: 0.85; }
|
|
.btn-primary { background: var(--langbot-accent, #2563eb); color: #fff; }
|
|
.btn-danger { background: #ef4444; color: #fff; }
|
|
.btn-ghost {
|
|
background: none; color: var(--langbot-text-muted, #71717a); border: 1px solid var(--langbot-border, #e4e4e7);
|
|
padding: 5px 10px; font-size: 12px;
|
|
}
|
|
.btn-ghost:hover { color: var(--langbot-text, #0a0a0a); }
|
|
|
|
.form-actions { display: flex; gap: 8px; justify-content: flex-end; }
|
|
|
|
/* Entry list */
|
|
.entry {
|
|
border: 1px solid var(--langbot-border, #e4e4e7); border-radius: 10px;
|
|
padding: 14px 16px; margin-bottom: 10px; background: var(--langbot-bg-card, #f8fafc);
|
|
transition: box-shadow 0.15s;
|
|
}
|
|
.entry:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
|
.entry-q { font-weight: 600; font-size: 14px; margin-bottom: 6px; }
|
|
.entry-a { font-size: 13px; color: var(--langbot-text-muted, #71717a); white-space: pre-wrap; line-height: 1.5; }
|
|
.entry-actions { display: flex; gap: 6px; margin-top: 10px; justify-content: flex-end; }
|
|
|
|
.empty {
|
|
text-align: center; padding: 40px 20px; color: var(--langbot-text-muted, #71717a);
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* Edit mode */
|
|
.edit-form input, .edit-form textarea {
|
|
width: 100%; padding: 6px 10px; border: 1px solid var(--langbot-border, #e4e4e7);
|
|
border-radius: 6px; background: var(--langbot-bg, #fff); color: var(--langbot-text, #0a0a0a);
|
|
font-size: 13px; font-family: inherit; outline: none; margin-bottom: 6px;
|
|
}
|
|
.edit-form textarea { resize: vertical; min-height: 50px; }
|
|
.edit-actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 4px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1 data-i18n="title">FAQ Manager</h1>
|
|
<span class="count" id="count"></span>
|
|
</div>
|
|
|
|
<div class="search-bar">
|
|
<input type="text" id="searchInput" data-i18n="searchPlaceholder" data-i18n-attr="placeholder" placeholder="Search FAQ...">
|
|
</div>
|
|
|
|
<div class="add-form" id="addForm">
|
|
<div class="form-title" data-i18n="addNew">Add New Entry</div>
|
|
<input type="text" id="questionInput" data-i18n="questionPlaceholder" data-i18n-attr="placeholder" placeholder="Question">
|
|
<textarea id="answerInput" rows="3" data-i18n="answerPlaceholder" data-i18n-attr="placeholder" placeholder="Answer"></textarea>
|
|
<div class="form-actions">
|
|
<button class="btn btn-primary" id="addBtn" data-i18n="add">Add</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="list"></div>
|
|
|
|
<script src="/api/v1/plugins/_sdk/page-sdk.js"></script>
|
|
<script>
|
|
var entries = [];
|
|
var searchQuery = '';
|
|
var editingId = null;
|
|
|
|
function renderList() {
|
|
var listEl = document.getElementById('list');
|
|
var filtered = entries;
|
|
if (searchQuery) {
|
|
var q = searchQuery.toLowerCase();
|
|
filtered = entries.filter(function(e) {
|
|
return e.question.toLowerCase().indexOf(q) !== -1 || e.answer.toLowerCase().indexOf(q) !== -1;
|
|
});
|
|
}
|
|
|
|
document.getElementById('count').textContent =
|
|
filtered.length + ' / ' + entries.length + ' ' + langbot.t('entries', 'entries');
|
|
|
|
// Entry fields reach the DOM as node text or as element properties only.
|
|
// They are never concatenated into markup, so a question cannot close an
|
|
// attribute or open a script/event-handler context.
|
|
listEl.replaceChildren();
|
|
if (filtered.length === 0) {
|
|
listEl.appendChild(el('div', 'empty', langbot.t('empty', 'No FAQ entries yet. Add one above.')));
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < filtered.length; i++) {
|
|
var e = filtered[i];
|
|
listEl.appendChild(editingId === e.id ? editForm(e) : entryRow(e));
|
|
}
|
|
}
|
|
|
|
function el(tag, className, text) {
|
|
var node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text !== undefined && text !== null) node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
function actionButton(label, className, handler) {
|
|
var button = el('button', className, label);
|
|
button.addEventListener('click', handler);
|
|
return button;
|
|
}
|
|
|
|
function entryRow(e) {
|
|
var row = el('div', 'entry');
|
|
row.appendChild(el('div', 'entry-q', 'Q: ' + e.question));
|
|
row.appendChild(el('div', 'entry-a', e.answer));
|
|
var actions = el('div', 'entry-actions');
|
|
actions.appendChild(actionButton(langbot.t('edit', 'Edit'), 'btn btn-ghost', function() {
|
|
startEdit(e.id);
|
|
}));
|
|
var remove = actionButton(langbot.t('delete', 'Delete'), 'btn btn-ghost', function() {
|
|
deleteEntry(e.id);
|
|
});
|
|
remove.style.color = '#ef4444';
|
|
remove.style.borderColor = '#fca5a5';
|
|
actions.appendChild(remove);
|
|
row.appendChild(actions);
|
|
return row;
|
|
}
|
|
|
|
function editForm(e) {
|
|
var box = el('div', 'entry edit-form');
|
|
var question = el('input');
|
|
question.type = 'text';
|
|
question.id = 'editQ';
|
|
question.value = e.question;
|
|
var answer = el('textarea');
|
|
answer.id = 'editA';
|
|
answer.rows = 3;
|
|
answer.value = e.answer;
|
|
var actions = el('div', 'edit-actions');
|
|
actions.appendChild(actionButton(langbot.t('cancel', 'Cancel'), 'btn btn-ghost', function() {
|
|
cancelEdit();
|
|
}));
|
|
actions.appendChild(actionButton(langbot.t('save', 'Save'), 'btn btn-primary', function() {
|
|
saveEdit(e.id);
|
|
}));
|
|
box.appendChild(question);
|
|
box.appendChild(answer);
|
|
box.appendChild(actions);
|
|
return box;
|
|
}
|
|
|
|
async function loadEntries() {
|
|
var data = await langbot.api('/entries', null, 'GET');
|
|
entries = (data && data.entries) || [];
|
|
renderList();
|
|
}
|
|
|
|
document.getElementById('addBtn').addEventListener('click', async function() {
|
|
var q = document.getElementById('questionInput').value.trim();
|
|
var a = document.getElementById('answerInput').value.trim();
|
|
if (!q || !a) return;
|
|
await langbot.api('/entries', { question: q, answer: a }, 'POST');
|
|
document.getElementById('questionInput').value = '';
|
|
document.getElementById('answerInput').value = '';
|
|
await loadEntries();
|
|
});
|
|
|
|
document.getElementById('searchInput').addEventListener('input', function(e) {
|
|
searchQuery = e.target.value;
|
|
renderList();
|
|
});
|
|
|
|
window.startEdit = function(id) {
|
|
editingId = id;
|
|
renderList();
|
|
};
|
|
|
|
window.cancelEdit = function() {
|
|
editingId = null;
|
|
renderList();
|
|
};
|
|
|
|
window.saveEdit = async function(id) {
|
|
var q = document.getElementById('editQ').value.trim();
|
|
var a = document.getElementById('editA').value.trim();
|
|
if (!q || !a) return;
|
|
await langbot.api('/entries', { id: id, question: q, answer: a }, 'PUT');
|
|
editingId = null;
|
|
await loadEntries();
|
|
};
|
|
|
|
window.deleteEntry = async function(id) {
|
|
await langbot.api('/entries', { id: id }, 'DELETE');
|
|
await loadEntries();
|
|
};
|
|
|
|
langbot.onReady(function() {
|
|
loadEntries();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|