diff --git a/src/App.svelte b/src/App.svelte index 35b9853..1ef17d5 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -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; 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) { + 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'}
WEBASSEMBLY PLUGINS

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.

{#if pluginError}
{pluginError}
{/if}{#if plugins.length}
{#each plugins as plugin}
{plugin.name}{plugin.plugin_id} · v{plugin.version}
API {plugin.manifest.api_version} · {plugin.manifest.permissions.length ? plugin.manifest.permissions.join(' · ') : 'NO PERMISSIONS'}
{plugin.enabled ? 'ENABLED' : 'DISABLED'}
{/each}
{:else}
No plugins are installed.
{/if}
{:else if activeView === 'Bot'} -
{commandFormId ? 'EDIT COMMAND' : 'NEW COMMAND'}
{#if commandVariableSuggestions.length}
{#each commandVariableSuggestions as variable}{/each}
{/if}
+
{commandFormId ? 'EDIT COMMAND' : 'NEW COMMAND'}
{#if commandVariableSuggestions.length}
{#each commandVariableSuggestions as variable}{/each}
{/if}
SIMULATION

Preview with literal sample data. Real requests are opt-in and use the same server-side safety checks.

{#if commandSimulationError}
{commandSimulationError}
{/if}{#if commandSimulationResult}
{commandSimulationResult.rendered ?? commandSimulationResult.error ?? 'Simulation completed.'}{#if commandSimulationResult.extracted_variables && Object.keys(commandSimulationResult.extracted_variables).length}Extracted: {formatExtractedVariables(commandSimulationResult.extracted_variables)}{/if}
{/if}
BOT COMMANDS
PUBLIC COMMAND CATALOG
{#if botCommandError}
{botCommandError}
{:else if botCommands.length}
{#each botCommands as command}
!{command.name}{command.description}{#if command.tags.length}
{command.tags.join(' · ')}
{/if}
{command.permission_level.toUpperCase()}{#if command.cooldown_seconds}{command.cooldown_seconds}s / {command.cooldown_scope}{/if}
{/each}
{:else}
No enabled commandsCreate commands on the server to make them available here.
{/if}
SECRETS MANAGEMENT
Values are encrypted and cannot be read after saving.
{#if botSecrets.length}
{#each botSecrets as secret}
{secret.name}{secret.description || 'No description'}{#if secret.used_by.length}
USED BY: {secret.used_by.join(', ')}
{/if}
{/each}
{:else}
There are no secrets yet.
{/if}{#if secretError}
{secretError}
{/if}
COMMAND MANAGEMENT
{#each botCommands as command}
!{command.name}{command.enabled ? 'ENABLED' : 'DISABLED'} · {command.permission_level.toUpperCase()}
{/each}{#if !botCommands.length}
No commands are configured yet.
{/if}
{:else if activeView === 'Collaboration'} diff --git a/src/styles.css b/src/styles.css index 39415a3..63a32e3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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;