summaryrefslogtreecommitdiffhomepage
path: root/src/features/chat/ui/ReasoningEffortSelector.svelte
blob: d982905202d7d5cb1f73e185ce72c39a595c556c (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>