summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/ui
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-12 20:38:57 +0900
committerAdam Malczewski <[email protected]>2026-06-12 20:38:57 +0900
commitbaa6f6c9d21de2f6ffc60e00f53c61d026155933 (patch)
treefecae91d99d906a7b5054b398e4d3d90894567a0 /src/features/chat/ui
parent7dcc06eecb5b691b0c0daec26db9d5e407d0a60e (diff)
downloaddispatch-web-baa6f6c9d21de2f6ffc60e00f53c61d026155933.tar.gz
dispatch-web-baa6f6c9d21de2f6ffc60e00f53c61d026155933.zip
feat(chat): reasoning-effort selector — sticky per-conversation thinking-depth knob
Consume the backend's reasoning-effort handoff ([email protected] ReasoningEffort + [email protected] GET/PUT /conversations/:id/reasoning-effort, ChatRequest.reasoningEffort): a 5-level selector in the sidebar Model view, under the provider + model dropdowns. null renders as 'high (default)' per the server-owned resolution chain; PUT on change (effective next turn); error + revert on 400; per-conversation re-mount incl. drafts (the draft id survives promotion, so an effort set on a draft applies from turn 1). Re-mirrored .dispatch references; GLOSSARY 'reasoning effort'; handoff updated. 616 tests green; live curl probe passed.
Diffstat (limited to 'src/features/chat/ui')
-rw-r--r--src/features/chat/ui/ReasoningEffortSelector.svelte75
1 files changed, 75 insertions, 0 deletions
diff --git a/src/features/chat/ui/ReasoningEffortSelector.svelte b/src/features/chat/ui/ReasoningEffortSelector.svelte
new file mode 100644
index 0000000..8c7b193
--- /dev/null
+++ b/src/features/chat/ui/ReasoningEffortSelector.svelte
@@ -0,0 +1,75 @@
+<script lang="ts">
+ import type { ReasoningEffort } from "@dispatch/transport-contract";
+ import {
+ effectiveEffort,
+ effortOptions,
+ isReasoningEffort,
+ type SaveReasoningEffort,
+ } from "../reasoning-effort";
+
+ let {
+ persisted,
+ save,
+ }: {
+ /** The conversation's persisted level, or null when never set (default applies). */
+ persisted: ReasoningEffort | null;
+ save: SaveReasoningEffort;
+ } = $props();
+
+ const options = effortOptions();
+
+ // The user's in-flight choice; null = mirror the (async-loaded) persisted prop.
+ // Re-mounted per conversation, so there is no cross-tab bleed.
+ let chosen = $state<ReasoningEffort | null>(null);
+ let saving = $state(false);
+ let error = $state<string | null>(null);
+ let justSaved = $state(false);
+
+ const selected = $derived(chosen ?? effectiveEffort(persisted));
+
+ async function handleChange(value: string) {
+ if (!isReasoningEffort(value) || saving) return;
+ chosen = value;
+ saving = true;
+ error = null;
+ justSaved = false;
+ const result = await save(value);
+ saving = false;
+ if (result === null) return;
+ if (result.ok) {
+ justSaved = true;
+ } else {
+ error = result.error;
+ chosen = null; // revert to the persisted value
+ }
+ }
+</script>
+
+<div class="flex flex-col gap-1">
+ <span class="text-xs font-semibold uppercase opacity-60">Reasoning effort</span>
+ <div class="flex items-center gap-2">
+ <select
+ class="select select-sm w-full"
+ value={selected}
+ disabled={saving}
+ onchange={(e) => handleChange(e.currentTarget.value)}
+ aria-label="Reasoning effort"
+ >
+ {#each options as option (option.value)}
+ <option value={option.value}>{option.label}</option>
+ {/each}
+ </select>
+ {#if saving}
+ <span class="loading loading-spinner loading-xs" aria-label="Saving reasoning effort"></span>
+ {/if}
+ </div>
+ {#if error}
+ <p class="text-xs text-error">{error}</p>
+ {:else if justSaved}
+ <p class="text-xs text-success">Saved — applies from the next turn.</p>
+ {:else}
+ <p class="text-xs opacity-50">
+ How long the model thinks before answering. Changing it can re-prefill the prompt cache once.
+ </p>
+ {/if}
+</div>