Add bot command definition editor

This commit is contained in:
2026-10-25 05:41:19 +01:00
parent f986cbb1e9
commit a422cc70f0
+79
View File
@@ -14,6 +14,8 @@
let authenticated = false;
let mustReplaceCredentials = false;
let username = '';
// Keeps the command-template example literal while Svelte parses the surrounding markup.
const user_name = '${user_name}';
let password = '';
let newUsername = '';
let newPassword = '';
@@ -78,6 +80,18 @@
let botCommands: Array<{ id: string; name: string; description: string; permission_level: string; aliases: string[]; tags: string[]; cooldown_seconds: number; cooldown_scope: string; response_template: string; error_template: string | null; enabled: boolean }> = [];
let publicBotCommands: Array<{ id: string; name: string; description: string; permission_level: string; aliases: string[]; tags: string[] }> = [];
let botCommandError = '';
let commandFormId = '';
let commandFormName = '';
let commandFormDescription = '';
let commandFormPermission = 'everyone';
let commandFormAliases = '';
let commandFormTags = '';
let commandFormCooldown = 0;
let commandFormCooldownScope = 'global';
let commandFormResponse = '';
let commandFormErrorResponse = '';
let commandFormEnabled = true;
let commandFormBusy = false;
let botSecrets: Array<{ id: string; name: string; description: string; used_by: string[] }> = [];
let secretName = '';
let secretDescription = '';
@@ -695,6 +709,70 @@
}
}
function resetCommandForm() {
commandFormId = '';
commandFormName = '';
commandFormDescription = '';
commandFormPermission = 'everyone';
commandFormAliases = '';
commandFormTags = '';
commandFormCooldown = 0;
commandFormCooldownScope = 'global';
commandFormResponse = '';
commandFormErrorResponse = '';
commandFormEnabled = true;
}
function editCommand(command: (typeof botCommands)[number]) {
commandFormId = command.id;
commandFormName = command.name;
commandFormDescription = command.description;
commandFormPermission = command.permission_level;
commandFormAliases = command.aliases.join(', ');
commandFormTags = command.tags.join(', ');
commandFormCooldown = command.cooldown_seconds;
commandFormCooldownScope = command.cooldown_scope;
commandFormResponse = command.response_template;
commandFormErrorResponse = command.error_template ?? '';
commandFormEnabled = command.enabled;
}
async function saveCommand() {
commandFormBusy = true;
botCommandError = '';
const aliases = commandFormAliases.split(',').map((value) => value.trim()).filter(Boolean);
const tags = commandFormTags.split(',').map((value) => value.trim()).filter(Boolean);
try {
const response = await fetch(commandFormId ? `${serverAddress}/api/v1/bot/commands/${encodeURIComponent(commandFormId)}` : `${serverAddress}/api/v1/bot/commands`, {
method: commandFormId ? 'PATCH' : 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${sessionToken}` },
body: JSON.stringify({ name: commandFormName, description: commandFormDescription, permission_level: commandFormPermission, aliases, tags, cooldown_seconds: Number(commandFormCooldown), cooldown_scope: commandFormCooldownScope, response_template: commandFormResponse, error_template: commandFormErrorResponse || null, enabled: commandFormEnabled }),
});
if (!response.ok) throw new Error('The command could not be saved. Check its name, aliases, and fields.');
resetCommandForm();
await loadBotCommands();
await loadPublicCommands();
} catch (error) {
botCommandError = error instanceof Error ? error.message : 'The command could not be saved.';
} finally {
commandFormBusy = false;
}
}
async function deleteCommand(id: string) {
if (!window.confirm('Delete this command permanently?')) return;
botCommandError = '';
try {
const response = await fetch(`${serverAddress}/api/v1/bot/commands/${encodeURIComponent(id)}`, { method: 'DELETE', headers: { authorization: `Bearer ${sessionToken}` } });
if (!response.ok) throw new Error('The command could not be deleted.');
if (commandFormId === id) resetCommandForm();
await loadBotCommands();
await loadPublicCommands();
} catch (error) {
botCommandError = error instanceof Error ? error.message : 'The command could not be deleted.';
}
}
async function loadPublicCommands() {
try {
const response = await fetch(`${serverAddress}/api/v1/bot/commands`);
@@ -1041,6 +1119,7 @@
</div>
</section>
{:else if activeView === 'Bot'}
<section class="bot-command-editor panel"><div class="panel-header"><span>{commandFormId ? 'EDIT COMMAND' : 'NEW COMMAND'}</span><button class="secondary" onclick={resetCommandForm}>CLEAR</button></div><div class="settings-body command-form"><div class="command-form-grid"><label class="micro" for="command-name">NAME</label><input id="command-name" class="control" bind:value={commandFormName} maxlength="64" placeholder="uptime" /><label class="micro" for="command-description">DESCRIPTION</label><input id="command-description" class="control" bind:value={commandFormDescription} maxlength="500" placeholder="What this command does" /><label class="micro" for="command-permission">PERMISSION</label><select id="command-permission" class="control" bind:value={commandFormPermission}><option value="everyone">EVERYONE</option><option value="subscriber">SUBSCRIBER</option><option value="moderator">MODERATOR</option><option value="broadcaster">BROADCASTER</option></select><label class="micro" for="command-aliases">ALIASES</label><input id="command-aliases" class="control" bind:value={commandFormAliases} placeholder="alias1, alias2" /><label class="micro" for="command-tags">TAGS</label><input id="command-tags" class="control" bind:value={commandFormTags} placeholder="utility, fun" /><label class="micro" for="command-cooldown">COOLDOWN (SECONDS)</label><input id="command-cooldown" class="control" type="number" min="0" max="86400" bind:value={commandFormCooldown} /><label class="micro" for="command-cooldown-scope">COOLDOWN SCOPE</label><select id="command-cooldown-scope" class="control" bind:value={commandFormCooldownScope}><option value="global">GLOBAL</option><option value="user">PER USER</option><option value="role">PER ROLE</option></select><label class="micro" for="command-response">RESPONSE TEMPLATE</label><textarea id="command-response" class="control command-template" bind:value={commandFormResponse} maxlength="2000" placeholder="Hello ${user_name}"></textarea><label class="micro" for="command-error-response">ERROR RESPONSE (OPTIONAL)</label><textarea id="command-error-response" class="control command-template" bind:value={commandFormErrorResponse} maxlength="2000" placeholder="Something went wrong."></textarea></div><label class="overlay-enabled"><input type="checkbox" bind:checked={commandFormEnabled} /> ENABLED</label><button class="primary control-button" onclick={saveCommand} disabled={commandFormBusy || !commandFormName || !commandFormDescription || !commandFormResponse}>{commandFormBusy ? 'SAVING…' : commandFormId ? 'SAVE COMMAND' : 'CREATE COMMAND'}</button></div></section>
<section class="bot-page panel"><div class="panel-header"><span>BOT COMMANDS</span><button class="secondary" onclick={loadBotCommands}>REFRESH</button></div><div class="bot-page-body"><div class="micro">PUBLIC COMMAND CATALOG</div>{#if botCommandError}<div class="auth-error">{botCommandError}</div>{:else if botCommands.length}<div class="bot-command-list">{#each botCommands as command}<article class="bot-command-row"><div><strong>!{command.name}</strong><span class="bot-command-description">{command.description}</span>{#if command.tags.length}<div class="micro">{command.tags.join(' · ')}</div>{/if}</div><div class="bot-command-meta"><span>{command.permission_level.toUpperCase()}</span>{#if command.cooldown_seconds}<span>{command.cooldown_seconds}s / {command.cooldown_scope}</span>{/if}</div></article>{/each}</div>{:else}<div class="empty-state"><strong>No enabled commands</strong><span>Create commands on the server to make them available here.</span></div>{/if}<div class="bot-secret-section"><div class="micro">SECRETS MANAGEMENT</div><div class="bot-secret-note">Values are encrypted and cannot be read after saving.</div><div class="secret-form"><input class="control" bind:value={secretName} maxlength="100" placeholder="Name" /><input class="control" bind:value={secretDescription} maxlength="500" placeholder="Description" /><input class="control" type="password" bind:value={secretValue} maxlength="65536" placeholder="Value" /><button class="primary" onclick={createSecret} disabled={!secretName || !secretValue}>ADD SECRET</button></div>{#if botSecrets.length}<div class="bot-secret-list">{#each botSecrets as secret}<div class="bot-secret-row"><div><strong>{secret.name}</strong><span>{secret.description || 'No description'}</span>{#if secret.used_by.length}<div class="micro">USED BY: {secret.used_by.join(', ')}</div>{/if}</div><button class="secondary-action" onclick={() => deleteSecret(secret.id)}>DELETE</button></div>{/each}</div>{:else}<div class="empty-row">There are no secrets yet.</div>{/if}{#if secretError}<div class="auth-error">{secretError}</div>{/if}</div></div></section>
{:else if activeView === 'Overlays'}
<section class="overlay-page panel"><div class="panel-header"><span>OVERLAYS</span><div class="overlay-toolbar"><button class="secondary" onclick={createOverlay}>NEW</button><button class="secondary" onclick={loadOverlays}>REFRESH</button></div></div><div class="overlay-layout"><aside class="overlay-list">{#each overlays as overlay}<button class:active={selectedOverlayId === overlay.id} class="overlay-list-item" onclick={() => selectOverlay(overlay)}><strong>{overlay.name}</strong><span>{overlay.published ? 'PUBLISHED' : 'DRAFT'}</span></button>{/each}{#if !overlays.length}<div class="empty-row">No overlays yet.</div>{/if}</aside><div class="overlay-editor">{#if selectedOverlayId || overlayName}<div class="overlay-fields"><input class="control" bind:value={overlayName} maxlength="100" placeholder="Overlay name" /><input class="control" bind:value={overlaySlug} maxlength="100" placeholder="stable-slug" /><label class="overlay-enabled"><input type="checkbox" bind:checked={overlayEnabled} /> ENABLED</label><input class="control" bind:value={overlayDisabledDesign} maxlength="10000" placeholder="Disabled design" /></div><div class="overlay-tabs"><label class="micro" for="overlay-html">HTML</label><textarea id="overlay-html" class="overlay-code" bind:value={overlayHtml} spellcheck="false"></textarea><label class="micro" for="overlay-css">CSS</label><textarea id="overlay-css" class="overlay-code" bind:value={overlayCss} spellcheck="false"></textarea><label class="micro" for="overlay-javascript">JAVASCRIPT</label><textarea id="overlay-javascript" class="overlay-code" bind:value={overlayJavascript} spellcheck="false"></textarea></div><div class="overlay-preview-label micro">LIVE PREVIEW</div><iframe class="overlay-preview" title="Overlay preview" sandbox="allow-scripts" srcdoc={`<style>${overlayCss}</style>${overlayEnabled ? overlayHtml : `<div>${overlayDisabledDesign}</div>`}\u003cscript\u003e${overlayJavascript}\u003c/script\u003e`}></iframe><div class="overlay-controls"><button class="primary" onclick={saveOverlay} disabled={overlayBusy || !overlayName || !overlaySlug}>SAVE DRAFT</button><button class="secondary" onclick={publishOverlay} disabled={overlayBusy || !selectedOverlayId}>PUBLISH</button><button class="secondary" onclick={undoOverlay} disabled={overlayBusy || !selectedOverlayId}>UNDO PUBLISHED</button><button class="secondary" onclick={copyOverlayUrl} disabled={!overlaySlug}>COPY OBS URL</button></div>{#if overlayError}<div class="auth-error">{overlayError}</div>{/if}{:else}<div class="empty-state"><strong>Select or create an overlay</strong><span>Drafts autosave when explicitly saved; publishing controls the browser source.</span></div>{/if}</div></div></section>