summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/ui/ReasoningEffortSelector.svelte
blob: 8c7b1931536af2bfd75763bb483436db57564395 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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>