summaryrefslogtreecommitdiffhomepage
path: root/src/features/system-prompt/ui/SystemPromptBuilder.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/system-prompt/ui/SystemPromptBuilder.svelte')
-rw-r--r--src/features/system-prompt/ui/SystemPromptBuilder.svelte467
1 files changed, 234 insertions, 233 deletions
diff --git a/src/features/system-prompt/ui/SystemPromptBuilder.svelte b/src/features/system-prompt/ui/SystemPromptBuilder.svelte
index 4e07317..2e92e26 100644
--- a/src/features/system-prompt/ui/SystemPromptBuilder.svelte
+++ b/src/features/system-prompt/ui/SystemPromptBuilder.svelte
@@ -1,242 +1,243 @@
<script lang="ts">
- import type { SystemPromptVariable } from "@dispatch/transport-contract";
- import { tick, untrack } from "svelte";
- import {
- buildTag,
- groupVariables,
- insertTag,
- isDynamicVariable,
- type LoadSystemPrompt,
- type LoadSystemPromptVariables,
- type SaveSystemPrompt,
- } from "../logic/view-model";
-
- let {
- loadPrompt,
- savePrompt,
- loadVariables,
- onClose,
- }: {
- loadPrompt: LoadSystemPrompt;
- savePrompt: SaveSystemPrompt;
- loadVariables: LoadSystemPromptVariables;
- onClose: () => void;
- } = $props();
-
- let value = $state("");
- let loadedTemplate = $state("");
- let loading = $state(false);
- let saving = $state(false);
- let error = $state<string | null>(null);
- let justSaved = $state(false);
- let variables = $state<readonly SystemPromptVariable[]>([]);
- let filePath = $state("");
- let textarea: HTMLTextAreaElement | null = null;
-
- const groups = $derived(groupVariables(variables));
- const hasChanges = $derived(value !== loadedTemplate);
-
- async function load() {
- untrack(() => {
- loading = true;
- error = null;
- });
-
- const [templateResult, variablesResult] = await Promise.all([loadPrompt(), loadVariables()]);
-
- loading = false;
-
- if (!templateResult.ok || !variablesResult.ok) {
- const parts: string[] = [];
- if (!templateResult.ok) parts.push(templateResult.error);
- if (!variablesResult.ok) parts.push(variablesResult.error);
- error = parts.join("; ");
- return;
- }
-
- value = templateResult.template;
- loadedTemplate = templateResult.template;
- variables = variablesResult.variables;
- }
-
- async function save() {
- if (saving || loading) return;
- saving = true;
- error = null;
- justSaved = false;
- const result = await savePrompt(value);
- saving = false;
- if (!result.ok) {
- error = result.error;
- return;
- }
- loadedTemplate = result.template;
- value = result.template;
- justSaved = true;
- }
-
- function reset() {
- value = loadedTemplate;
- error = null;
- justSaved = false;
- }
-
- async function insertTagAtCursor(tag: string) {
- if (textarea === null) return;
- const start = textarea.selectionStart;
- const end = textarea.selectionEnd;
- const insertion = insertTag(value, tag, start, end);
- value = insertion.template;
- await tick();
- textarea.focus();
- textarea.setSelectionRange(insertion.cursor, insertion.cursor);
- }
-
- async function insertFileVariable(type: string) {
- const path = filePath.trim();
- if (path.length === 0) return;
- await insertTagAtCursor(buildTag(type, path));
- filePath = "";
- }
-
- function onKeydown(e: KeyboardEvent) {
- if (e.key === "Escape") onClose();
- }
-
- // Load on mount once.
- $effect(() => {
- void load();
- });
+ import type { SystemPromptVariable } from "@dispatch/transport-contract";
+ import { tick, untrack } from "svelte";
+ import {
+ buildTag,
+ groupVariables,
+ insertTag,
+ isDynamicVariable,
+ type LoadSystemPrompt,
+ type LoadSystemPromptVariables,
+ type SaveSystemPrompt,
+ } from "../logic/view-model";
+
+ let {
+ loadPrompt,
+ savePrompt,
+ loadVariables,
+ onClose,
+ }: {
+ loadPrompt: LoadSystemPrompt;
+ savePrompt: SaveSystemPrompt;
+ loadVariables: LoadSystemPromptVariables;
+ onClose: () => void;
+ } = $props();
+
+ let value = $state("");
+ let loadedTemplate = $state("");
+ let loading = $state(false);
+ let saving = $state(false);
+ let error = $state<string | null>(null);
+ let justSaved = $state(false);
+ let variables = $state<readonly SystemPromptVariable[]>([]);
+ let filePath = $state("");
+ let textarea: HTMLTextAreaElement | null = null;
+
+ const groups = $derived(groupVariables(variables));
+ const hasChanges = $derived(value !== loadedTemplate);
+
+ async function load() {
+ untrack(() => {
+ loading = true;
+ error = null;
+ });
+
+ const [templateResult, variablesResult] = await Promise.all([loadPrompt(), loadVariables()]);
+
+ loading = false;
+
+ if (!templateResult.ok || !variablesResult.ok) {
+ const parts: string[] = [];
+ if (!templateResult.ok) parts.push(templateResult.error);
+ if (!variablesResult.ok) parts.push(variablesResult.error);
+ error = parts.join("; ");
+ return;
+ }
+
+ value = templateResult.template;
+ loadedTemplate = templateResult.template;
+ variables = variablesResult.variables;
+ }
+
+ async function save() {
+ if (saving || loading) return;
+ saving = true;
+ error = null;
+ justSaved = false;
+ const result = await savePrompt(value);
+ saving = false;
+ if (!result.ok) {
+ error = result.error;
+ return;
+ }
+ loadedTemplate = result.template;
+ value = result.template;
+ justSaved = true;
+ }
+
+ function reset() {
+ value = loadedTemplate;
+ error = null;
+ justSaved = false;
+ }
+
+ async function insertTagAtCursor(tag: string) {
+ if (textarea === null) return;
+ const start = textarea.selectionStart;
+ const end = textarea.selectionEnd;
+ const insertion = insertTag(value, tag, start, end);
+ value = insertion.template;
+ await tick();
+ textarea.focus();
+ textarea.setSelectionRange(insertion.cursor, insertion.cursor);
+ }
+
+ async function insertFileVariable(type: string) {
+ const path = filePath.trim();
+ if (path.length === 0) return;
+ await insertTagAtCursor(buildTag(type, path));
+ filePath = "";
+ }
+
+ function onKeydown(e: KeyboardEvent) {
+ if (e.key === "Escape") onClose();
+ }
+
+ // Load on mount once.
+ $effect(() => {
+ void load();
+ });
</script>
<svelte:window onkeydown={onKeydown} />
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
- class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
- role="dialog"
- aria-modal="true"
- aria-label="System prompt builder"
- tabindex="-1"
- onclick={onClose}
- onkeydown={onKeydown}
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
+ role="dialog"
+ aria-modal="true"
+ aria-label="System prompt builder"
+ tabindex="-1"
+ onclick={onClose}
+ onkeydown={onKeydown}
>
- <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
- <div
- class="flex h-[85vh] w-full max-w-6xl flex-col overflow-hidden rounded-box bg-base-100 shadow-2xl"
- onclick={(e) => e.stopPropagation()}
- >
- <!-- Header -->
- <div class="flex shrink-0 items-center justify-between border-b border-base-300 px-4 py-3">
- <div class="flex items-center gap-2">
- <h2 class="text-sm font-semibold">System Prompt</h2>
- {#if loading}
- <span class="loading loading-spinner loading-xs"></span>
- {/if}
- </div>
- <button
- type="button"
- class="btn btn-ghost btn-sm btn-square"
- onclick={onClose}
- aria-label="Close system prompt builder"
- >
- ✕
- </button>
- </div>
-
- <!-- Body: half editor / half variables -->
- <div class="flex min-h-0 flex-1">
- <!-- Left: template editor -->
- <div class="flex w-1/2 min-w-0 flex-col gap-2 border-r border-base-300 p-4">
- <textarea
- bind:this={textarea}
- bind:value
- class="textarea textarea-bordered min-h-0 w-full flex-1 resize-none font-mono text-xs"
- placeholder={loading ? "Loading template..." : "Edit the global system prompt template..."}
- disabled={loading}
- aria-label="System prompt template"
- ></textarea>
-
- <div class="flex shrink-0 items-center gap-2">
- <button
- type="button"
- class="btn btn-primary btn-sm"
- disabled={loading || saving || !hasChanges}
- onclick={save}
- >
- {#if saving}
- <span class="loading loading-spinner loading-xs"></span>
- {:else}
- Save
- {/if}
- </button>
- <button
- type="button"
- class="btn btn-ghost btn-sm"
- disabled={loading || !hasChanges}
- onclick={reset}
- >
- Reset
- </button>
- {#if justSaved && !hasChanges}
- <span class="text-xs text-success">Saved.</span>
- {:else if hasChanges}
- <span class="text-xs opacity-60">Unsaved changes</span>
- {/if}
- </div>
-
- {#if error}
- <p class="shrink-0 text-xs text-error">{error}</p>
- {/if}
- </div>
-
- <!-- Right: variable palette -->
- <div class="flex w-1/2 min-w-0 flex-col overflow-y-auto p-4">
- <h3 class="mb-2 shrink-0 text-xs font-semibold uppercase opacity-60">Variables</h3>
- {#if groups.length === 0 && !loading}
- <p class="text-xs opacity-60">No variables available.</p>
- {/if}
- <div class="flex flex-col gap-3">
- {#each groups as group (group.type)}
- <div class="rounded-box bg-base-200 p-3">
- <span class="text-xs font-semibold uppercase opacity-70">{group.type}</span>
- <div class="mt-2 flex flex-wrap gap-1">
- {#each group.variables as variable (variable.type + variable.name)}
- {#if isDynamicVariable(variable)}
- <div class="flex items-center gap-1">
- <input
- type="text"
- class="input input-bordered input-xs w-32 font-mono"
- bind:value={filePath}
- placeholder={variable.name}
- onkeydown={(e) => {
- if (e.key === "Enter") void insertFileVariable(variable.type);
- }}
- />
- <button
- type="button"
- class="btn btn-xs"
- onclick={() => void insertFileVariable(variable.type)}
- >
- Insert
- </button>
- </div>
- {:else}
- <button
- type="button"
- class="btn btn-xs"
- title={variable.description}
- onclick={() => void insertTagAtCursor(buildTag(variable.type, variable.name))}
- >
- {variable.name}
- </button>
- {/if}
- {/each}
- </div>
- </div>
- {/each}
- </div>
- </div>
- </div>
- </div>
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
+ <div
+ class="flex h-[85vh] w-full max-w-6xl flex-col overflow-hidden rounded-box bg-base-100 shadow-2xl"
+ onclick={(e) => e.stopPropagation()}
+ >
+ <!-- Header -->
+ <div class="flex shrink-0 items-center justify-between border-b border-base-300 px-4 py-3">
+ <div class="flex items-center gap-2">
+ <h2 class="text-sm font-semibold">System Prompt</h2>
+ {#if loading}
+ <span class="loading loading-spinner loading-xs"></span>
+ {/if}
+ </div>
+ <button
+ type="button"
+ class="btn btn-ghost btn-sm btn-square"
+ onclick={onClose}
+ aria-label="Close system prompt builder"
+ >
+ ✕
+ </button>
+ </div>
+
+ <!-- Body: half editor / half variables -->
+ <div class="flex min-h-0 flex-1">
+ <!-- Left: template editor -->
+ <div class="flex w-1/2 min-w-0 flex-col gap-2 border-r border-base-300 p-4">
+ <textarea
+ bind:this={textarea}
+ bind:value
+ class="textarea textarea-bordered min-h-0 w-full flex-1 resize-none font-mono text-xs"
+ placeholder={loading
+ ? "Loading template..."
+ : "Edit the global system prompt template..."}
+ disabled={loading}
+ aria-label="System prompt template"></textarea>
+
+ <div class="flex shrink-0 items-center gap-2">
+ <button
+ type="button"
+ class="btn btn-primary btn-sm"
+ disabled={loading || saving || !hasChanges}
+ onclick={save}
+ >
+ {#if saving}
+ <span class="loading loading-spinner loading-xs"></span>
+ {:else}
+ Save
+ {/if}
+ </button>
+ <button
+ type="button"
+ class="btn btn-ghost btn-sm"
+ disabled={loading || !hasChanges}
+ onclick={reset}
+ >
+ Reset
+ </button>
+ {#if justSaved && !hasChanges}
+ <span class="text-xs text-success">Saved.</span>
+ {:else if hasChanges}
+ <span class="text-xs opacity-60">Unsaved changes</span>
+ {/if}
+ </div>
+
+ {#if error}
+ <p class="shrink-0 text-xs text-error">{error}</p>
+ {/if}
+ </div>
+
+ <!-- Right: variable palette -->
+ <div class="flex w-1/2 min-w-0 flex-col overflow-y-auto p-4">
+ <h3 class="mb-2 shrink-0 text-xs font-semibold uppercase opacity-60">Variables</h3>
+ {#if groups.length === 0 && !loading}
+ <p class="text-xs opacity-60">No variables available.</p>
+ {/if}
+ <div class="flex flex-col gap-3">
+ {#each groups as group (group.type)}
+ <div class="rounded-box bg-base-200 p-3">
+ <span class="text-xs font-semibold uppercase opacity-70">{group.type}</span>
+ <div class="mt-2 flex flex-wrap gap-1">
+ {#each group.variables as variable (variable.type + variable.name)}
+ {#if isDynamicVariable(variable)}
+ <div class="flex items-center gap-1">
+ <input
+ type="text"
+ class="input input-bordered input-xs w-32 font-mono"
+ bind:value={filePath}
+ placeholder={variable.name}
+ onkeydown={(e) => {
+ if (e.key === "Enter") void insertFileVariable(variable.type);
+ }}
+ />
+ <button
+ type="button"
+ class="btn btn-xs"
+ onclick={() => void insertFileVariable(variable.type)}
+ >
+ Insert
+ </button>
+ </div>
+ {:else}
+ <button
+ type="button"
+ class="btn btn-xs"
+ title={variable.description}
+ onclick={() => void insertTagAtCursor(buildTag(variable.type, variable.name))}
+ >
+ {variable.name}
+ </button>
+ {/if}
+ {/each}
+ </div>
+ </div>
+ {/each}
+ </div>
+ </div>
+ </div>
+ </div>
</div>