Add command simulation controls
This commit is contained in:
+47
-1
@@ -167,6 +167,10 @@
|
||||
let commandFormErrorResponse = '';
|
||||
let commandFormEnabled = true;
|
||||
let commandFormBusy = false;
|
||||
let commandSimulationBusy = false;
|
||||
let commandSimulationError = '';
|
||||
let commandSimulationResult: { rendered?: string | null; extracted_variables?: Record<string, string>; error?: string | null } | null = null;
|
||||
let commandSimulationSampleResponse = '{"status":"ok"}';
|
||||
let commandVariableSuggestions: string[] = [];
|
||||
let botSecrets: Array<{ id: string; name: string; description: string; used_by: string[] }> = [];
|
||||
let secretName = '';
|
||||
@@ -823,6 +827,8 @@
|
||||
commandFormResponse = '';
|
||||
commandFormErrorResponse = '';
|
||||
commandVariableSuggestions = [];
|
||||
commandSimulationError = '';
|
||||
commandSimulationResult = null;
|
||||
commandFormEnabled = true;
|
||||
}
|
||||
|
||||
@@ -839,6 +845,10 @@
|
||||
commandVariableSuggestions = [];
|
||||
}
|
||||
|
||||
function formatExtractedVariables(values: Record<string, string>) {
|
||||
return Object.entries(values).map(([key, value]) => `${key}=${value}`).join(' · ');
|
||||
}
|
||||
|
||||
function editCommand(command: (typeof botCommands)[number]) {
|
||||
commandFormId = command.id;
|
||||
commandFormName = command.name;
|
||||
@@ -875,6 +885,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function simulateCommand(useRealRequest = false) {
|
||||
commandSimulationBusy = true;
|
||||
commandSimulationError = '';
|
||||
commandSimulationResult = null;
|
||||
let sampleResponse: unknown = undefined;
|
||||
if (!useRealRequest) {
|
||||
try {
|
||||
sampleResponse = JSON.parse(commandSimulationSampleResponse);
|
||||
} catch {
|
||||
commandSimulationBusy = false;
|
||||
commandSimulationError = 'Sample response must contain valid JSON.';
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${serverAddress}/api/v1/bot/commands/simulate`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', authorization: `Bearer ${sessionToken}` },
|
||||
body: JSON.stringify({
|
||||
template: commandFormResponse,
|
||||
variables: { user_name: 'PreviewUser', user_id: 'preview', message: '!preview', args: 'sample' },
|
||||
sample_response: sampleResponse,
|
||||
use_real_request: useRealRequest,
|
||||
response_fields: [],
|
||||
}),
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(result.error || 'The command simulation failed.');
|
||||
commandSimulationResult = result;
|
||||
} catch (error) {
|
||||
commandSimulationError = error instanceof Error ? error.message : 'The command simulation failed.';
|
||||
} finally {
|
||||
commandSimulationBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCommand(id: string) {
|
||||
if (!window.confirm('Delete this command permanently?')) return;
|
||||
botCommandError = '';
|
||||
@@ -1329,7 +1375,7 @@
|
||||
{:else if activeView === 'Plugins'}
|
||||
<section class="plugins-page panel"><div class="panel-header"><span>WEBASSEMBLY PLUGINS</span><button class="secondary" onclick={loadPlugins}>REFRESH</button></div><div class="settings-body"><p class="settings-note">Packages are validated and started in a resource-limited WebAssembly runtime. Only explicitly declared Trixie permissions are accepted; installation never grants filesystem or process access.</p><label class:drop-active={pluginDropActive} class="plugin-drop-zone" ondragover={(event) => { event.preventDefault(); pluginDropActive = true; }} ondragleave={() => (pluginDropActive = false)} ondrop={handlePluginDrop}><strong>DROP PLUGIN PACKAGE HERE</strong><span>or choose a `.trixie-plugin` JSON package</span><input class="visually-hidden" type="file" accept=".trixie-plugin,.json,application/json" onchange={handlePluginFileInput} /></label><div class="overlay-controls"><label class="primary plugin-file-button">CHOOSE PACKAGE<input class="visually-hidden" type="file" accept=".trixie-plugin,.json,application/json" onchange={handlePluginFileInput} /></label></div>{#if pluginError}<div class="auth-error">{pluginError}</div>{/if}{#if plugins.length}<div class="plugin-list">{#each plugins as plugin}<article class="plugin-row"><div><strong>{plugin.name}</strong><span class="bot-command-description">{plugin.plugin_id} · v{plugin.version}</span><div class="micro">API {plugin.manifest.api_version} · {plugin.manifest.permissions.length ? plugin.manifest.permissions.join(' · ') : 'NO PERMISSIONS'}</div></div><div class="bot-command-meta"><span class:good={plugin.enabled}>{plugin.enabled ? 'ENABLED' : 'DISABLED'}</span><button class="secondary-action" onclick={() => setPluginEnabled(plugin)} disabled={pluginBusy}>{plugin.enabled ? 'DISABLE' : 'ENABLE'}</button><button class="secondary-action" onclick={() => rollbackPlugin(plugin)} disabled={pluginBusy}>ROLLBACK</button><button class="secondary-action danger" onclick={() => removePlugin(plugin)} disabled={pluginBusy}>REMOVE</button></div></article>{/each}</div>{:else}<div class="empty-row">No plugins are installed.</div>{/if}</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} oninput={() => updateCommandVariableSuggestions(commandFormResponse)} maxlength="2000" placeholder="Hello ${user_name}"></textarea>{#if commandVariableSuggestions.length}<div class="command-variable-picker" role="listbox" aria-label="Command variables">{#each commandVariableSuggestions as variable}<button class="secondary-action" type="button" onclick={() => insertCommandVariable(variable)}>${`{${variable}}`}</button>{/each}</div>{/if}<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-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} oninput={() => updateCommandVariableSuggestions(commandFormResponse)} maxlength="2000" placeholder="Hello ${user_name}"></textarea>{#if commandVariableSuggestions.length}<div class="command-variable-picker" role="listbox" aria-label="Command variables">{#each commandVariableSuggestions as variable}<button class="secondary-action" type="button" onclick={() => insertCommandVariable(variable)}>${`{${variable}}`}</button>{/each}</div>{/if}<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><div class="command-simulation panel"><div class="micro">SIMULATION</div><p class="bot-secret-note">Preview with literal sample data. Real requests are opt-in and use the same server-side safety checks.</p><textarea class="control command-template" bind:value={commandSimulationSampleResponse} aria-label="Sample JSON response" placeholder="status ok"></textarea><div class="command-simulation-actions"><button class="secondary" type="button" onclick={() => simulateCommand(false)} disabled={commandSimulationBusy || !commandFormResponse}>SAMPLE REQUEST</button><button class="secondary" type="button" onclick={() => simulateCommand(true)} disabled={commandSimulationBusy || !commandFormResponse}>REAL REQUEST</button></div>{#if commandSimulationError}<div class="auth-error">{commandSimulationError}</div>{/if}{#if commandSimulationResult}<div class="command-simulation-result"><strong>{commandSimulationResult.rendered ?? commandSimulationResult.error ?? 'Simulation completed.'}</strong>{#if commandSimulationResult.extracted_variables && Object.keys(commandSimulationResult.extracted_variables).length}<span>Extracted: {formatExtractedVariables(commandSimulationResult.extracted_variables)}</span>{/if}</div>{/if}</div><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>
|
||||
<section class="panel command-management-panel"><div class="panel-header"><span>COMMAND MANAGEMENT</span></div><div class="settings-body">{#each botCommands as command}<div class="bot-command-row"><div><strong>!{command.name}</strong><span class="bot-command-description">{command.enabled ? 'ENABLED' : 'DISABLED'} · {command.permission_level.toUpperCase()}</span></div><div class="bot-command-meta"><button class="secondary-action" onclick={() => editCommand(command)}>EDIT</button><button class="secondary-action danger" onclick={() => deleteCommand(command.id)}>DELETE</button></div></div>{/each}{#if !botCommands.length}<div class="empty-row">No commands are configured yet.</div>{/if}</div></section>
|
||||
{:else if activeView === 'Collaboration'}
|
||||
|
||||
@@ -555,6 +555,28 @@ label {
|
||||
color: var(--ed-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
.command-simulation {
|
||||
margin: 16px 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--ui-divider);
|
||||
background: var(--ui-panel);
|
||||
}
|
||||
.command-simulation-actions {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.command-simulation-result {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
margin-top: 10px;
|
||||
padding: 9px;
|
||||
border-left: 2px solid var(--ed-cyan);
|
||||
background: var(--ui-input);
|
||||
color: var(--ed-text);
|
||||
font: 11px/1.4 var(--font-data);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.secret-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr auto;
|
||||
|
||||
Reference in New Issue
Block a user