@ouraihub/aether-ui 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/chat/ChatComposer.svelte +47 -5
- package/src/chat/ChatPanel.css +380 -380
- package/src/chat/ChatPanel.svelte +48 -51
- package/src/chat/ChatPanelComposer.svelte +44 -25
- package/src/chat/ChatPanelHeader.svelte +1 -25
- package/src/chat/ChatPanelMode.svelte +3 -3
- package/src/chat/ChatPanelToolbar.svelte +14 -3
- package/src/chat/ContextUsageStatus.svelte +97 -0
- package/src/chat/ModelEffortPicker.svelte +326 -0
- package/src/chat/PickerEffort.svelte +71 -0
- package/src/chat/ReasoningControl.svelte +10 -112
- package/src/chat/ReasoningSlider.svelte +174 -0
- package/src/chat/SkillPicker.svelte +1 -0
- package/src/chat/UsageControl.svelte +12 -5
- package/src/chat/UsageDetails.svelte +19 -3
- package/src/chat/chat-attachments.svelte.ts +9 -5
- package/src/chat/chat-panel-models.svelte.ts +97 -0
- package/src/chat/chat-panel-props.ts +6 -0
- package/src/chat/context-status.ts +101 -0
- package/src/chat/icons/LICENSE-REMIX +186 -0
- package/src/chat/icons/LICENSE-SIMPLE-ICONS +30 -0
- package/src/chat/icons/PathIcon.svelte +8 -0
- package/src/chat/icons/effort-icons.ts +65 -0
- package/src/chat/icons/status-icons.ts +105 -0
- package/src/chat/index.ts +20 -0
- package/src/chat/model-effort.ts +177 -0
- package/src/chat/test-support/chat-fixture.ts +16 -0
- package/src/i18n/en-chat.d.ts +1 -0
- package/src/i18n/en-chat.js +201 -0
- package/src/i18n/en.ts +24 -174
- package/src/i18n/zh-CN-chat.d.ts +1 -0
- package/src/i18n/zh-CN-chat.js +190 -0
- package/src/i18n/zh-CN.ts +2 -158
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Composer pill + upward popover (docs/31 §4.2). The host owns data and every event's effect.
|
|
3
|
+
import { Command, Popover } from 'bits-ui'
|
|
4
|
+
import CheckIcon from '@lucide/svelte/icons/check'
|
|
5
|
+
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'
|
|
6
|
+
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
|
|
7
|
+
import KeyIcon from '@lucide/svelte/icons/key-round'
|
|
8
|
+
import PinIcon from '@lucide/svelte/icons/pin'
|
|
9
|
+
import RefreshIcon from '@lucide/svelte/icons/refresh-cw'
|
|
10
|
+
import SearchIcon from '@lucide/svelte/icons/search'
|
|
11
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
12
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
13
|
+
import { Separator } from '#lib/components/ui/separator/index.js'
|
|
14
|
+
import { cn } from '#lib/utils.js'
|
|
15
|
+
import type { KitI18n } from '../i18n'
|
|
16
|
+
import type { ReasoningEffort } from '../ports'
|
|
17
|
+
import { REASONING_EFFORTS } from '../providers/reasoning'
|
|
18
|
+
import PickerEffort from './PickerEffort.svelte'
|
|
19
|
+
import {
|
|
20
|
+
effortBars,
|
|
21
|
+
effortTone,
|
|
22
|
+
groupModels,
|
|
23
|
+
groupSummary,
|
|
24
|
+
nextValue,
|
|
25
|
+
type PickerModel,
|
|
26
|
+
type PickerValue,
|
|
27
|
+
} from './model-effort'
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
models,
|
|
31
|
+
value = null,
|
|
32
|
+
defaultValue = null,
|
|
33
|
+
disabled = false,
|
|
34
|
+
refreshing = false,
|
|
35
|
+
notice = '',
|
|
36
|
+
i18n,
|
|
37
|
+
open = $bindable(false),
|
|
38
|
+
onselect,
|
|
39
|
+
onsetdefault,
|
|
40
|
+
onrefresh,
|
|
41
|
+
onopensettings,
|
|
42
|
+
}: {
|
|
43
|
+
models: readonly PickerModel[]
|
|
44
|
+
value?: PickerValue | null
|
|
45
|
+
defaultValue?: PickerValue | null
|
|
46
|
+
disabled?: boolean
|
|
47
|
+
refreshing?: boolean
|
|
48
|
+
notice?: string
|
|
49
|
+
i18n: KitI18n
|
|
50
|
+
open?: boolean
|
|
51
|
+
onselect: (value: PickerValue) => void
|
|
52
|
+
onsetdefault?: ((value: PickerValue) => void) | undefined
|
|
53
|
+
onrefresh?: ((force: boolean) => void) | undefined
|
|
54
|
+
/** Opens host settings for the current model when it declares no reasoning levels. */
|
|
55
|
+
onopensettings?: (() => void) | undefined
|
|
56
|
+
} = $props()
|
|
57
|
+
const t = (key: string, params?: Record<string, string | number>) => i18n.t(key, params)
|
|
58
|
+
let query = $state('')
|
|
59
|
+
// Command tracks the highlighted item; arrows/Enter come from the primitive.
|
|
60
|
+
let highlighted = $state('')
|
|
61
|
+
let trigger = $state<HTMLButtonElement | null>(null)
|
|
62
|
+
// Groups opened while browsing, and groups folded while searching (searching opens all).
|
|
63
|
+
const expanded = new SvelteSet<string>()
|
|
64
|
+
const searchFolded = new SvelteSet<string>()
|
|
65
|
+
const searching = $derived(query.trim() !== '')
|
|
66
|
+
// Hosts may declare levels in any order; the rail always reads 关闭 → 最大.
|
|
67
|
+
const current = $derived.by(() => {
|
|
68
|
+
const found = models.find((model) => model.key === value?.key)
|
|
69
|
+
return found
|
|
70
|
+
? {
|
|
71
|
+
...found,
|
|
72
|
+
efforts: REASONING_EFFORTS.filter((level) => found.efforts.includes(level)),
|
|
73
|
+
}
|
|
74
|
+
: null
|
|
75
|
+
})
|
|
76
|
+
const effort: ReasoningEffort = $derived(
|
|
77
|
+
current && value && current.efforts.includes(value.effort) ? value.effort : 'default',
|
|
78
|
+
)
|
|
79
|
+
const effortLabel = $derived(t(`chat.effort.${effort}`))
|
|
80
|
+
const name = $derived(current?.name ?? t('chat.picker.choose'))
|
|
81
|
+
const bars = $derived(current ? effortBars(current, effort) : 0)
|
|
82
|
+
const tone = $derived(effortTone(effort))
|
|
83
|
+
const groups = $derived(groupModels(models, query))
|
|
84
|
+
const isOpen = (id: string) => (searching ? !searchFolded.has(id) : expanded.has(id))
|
|
85
|
+
const sameAsDefault = $derived(
|
|
86
|
+
value !== null && value.key === defaultValue?.key && value.effort === defaultValue.effort,
|
|
87
|
+
)
|
|
88
|
+
$effect(() => {
|
|
89
|
+
// A new current model reveals its group; other groups keep the user's folding.
|
|
90
|
+
if (current) expanded.add(current.providerId)
|
|
91
|
+
})
|
|
92
|
+
function openChanged(next: boolean) {
|
|
93
|
+
if (!next) return
|
|
94
|
+
query = ''
|
|
95
|
+
highlighted = value?.key ?? ''
|
|
96
|
+
searchFolded.clear()
|
|
97
|
+
onrefresh?.(false)
|
|
98
|
+
}
|
|
99
|
+
function toggle(id: string) {
|
|
100
|
+
const set = searching ? searchFolded : expanded
|
|
101
|
+
if (!set.delete(id)) set.add(id)
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<Popover.Root bind:open onOpenChange={openChanged}>
|
|
106
|
+
<Popover.Trigger {disabled}>
|
|
107
|
+
{#snippet child({ props })}
|
|
108
|
+
<Button
|
|
109
|
+
{...props}
|
|
110
|
+
bind:ref={trigger}
|
|
111
|
+
variant="ghost"
|
|
112
|
+
size="sm"
|
|
113
|
+
class="min-w-0 max-w-70 shrink"
|
|
114
|
+
data-composer-model
|
|
115
|
+
aria-label={t('chat.picker.trigger', { name, effort: effortLabel })}
|
|
116
|
+
>
|
|
117
|
+
<span
|
|
118
|
+
data-composer-model-detail
|
|
119
|
+
class="text-kit-xs text-muted-foreground"
|
|
120
|
+
aria-hidden="true">{current?.provider.charAt(0).toUpperCase() ?? '·'}</span
|
|
121
|
+
>
|
|
122
|
+
<span class="min-w-0 truncate">{name}</span>
|
|
123
|
+
{#if current && effort !== 'default'}
|
|
124
|
+
<span class="inline-flex h-3 shrink-0 items-end gap-0.5" aria-hidden="true">
|
|
125
|
+
{#each ['h-1', 'h-1.5', 'h-2', 'h-3'] as height, bar (height)}
|
|
126
|
+
<span
|
|
127
|
+
class={cn(
|
|
128
|
+
'w-0.5 rounded-full bg-border',
|
|
129
|
+
height,
|
|
130
|
+
bar < bars && tone === 'fast' && 'bg-info',
|
|
131
|
+
bar < bars && tone === 'regular' && 'bg-primary',
|
|
132
|
+
bar < bars && tone === 'deep' && 'bg-warning',
|
|
133
|
+
)}
|
|
134
|
+
></span>
|
|
135
|
+
{/each}
|
|
136
|
+
</span>
|
|
137
|
+
<span data-composer-model-detail class="text-kit-xs text-muted-foreground"
|
|
138
|
+
>{effortLabel}</span
|
|
139
|
+
>
|
|
140
|
+
{/if}
|
|
141
|
+
{#if current?.hasKey}<KeyIcon
|
|
142
|
+
data-composer-model-detail
|
|
143
|
+
data-icon="inline-end"
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
/>{/if}
|
|
146
|
+
<ChevronDownIcon data-icon="inline-end" aria-hidden="true" />
|
|
147
|
+
</Button>
|
|
148
|
+
{/snippet}
|
|
149
|
+
</Popover.Trigger>
|
|
150
|
+
<Popover.Portal>
|
|
151
|
+
<Popover.Content
|
|
152
|
+
class={cn(
|
|
153
|
+
'z-[210] flex w-[min(360px,calc(100vw-16px))] flex-col overflow-hidden rounded-kit',
|
|
154
|
+
'max-h-[min(calc(100dvh-64px),var(--bits-popover-content-available-height))]',
|
|
155
|
+
'border bg-popover text-xs text-popover-foreground shadow-lg',
|
|
156
|
+
)}
|
|
157
|
+
side="top"
|
|
158
|
+
align="start"
|
|
159
|
+
sideOffset={4}
|
|
160
|
+
collisionPadding={8}
|
|
161
|
+
trapFocus={false}
|
|
162
|
+
preventScroll={false}
|
|
163
|
+
onEscapeKeydown={(event) => {
|
|
164
|
+
if (event.isComposing) event.preventDefault()
|
|
165
|
+
event.stopPropagation()
|
|
166
|
+
}}
|
|
167
|
+
onCloseAutoFocus={(event) => {
|
|
168
|
+
event.preventDefault()
|
|
169
|
+
trigger?.focus({ preventScroll: true })
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<Command.Root
|
|
173
|
+
bind:value={highlighted}
|
|
174
|
+
shouldFilter={false}
|
|
175
|
+
loop
|
|
176
|
+
label={t('chat.picker.choose')}
|
|
177
|
+
class="flex min-h-0 flex-col"
|
|
178
|
+
>
|
|
179
|
+
<div class="flex items-center gap-1.5 px-2 text-muted-foreground">
|
|
180
|
+
<SearchIcon class="size-3.5 shrink-0" aria-hidden="true" />
|
|
181
|
+
<Command.Input
|
|
182
|
+
bind:value={query}
|
|
183
|
+
placeholder={t('chat.picker.search')}
|
|
184
|
+
aria-label={t('chat.picker.searchLabel')}
|
|
185
|
+
class="h-8 min-w-0 flex-1 bg-transparent text-foreground outline-none"
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
<Separator />
|
|
189
|
+
<Command.List class="max-h-[clamp(48px,calc(100dvh-250px),248px)] overflow-y-auto p-1">
|
|
190
|
+
{#if models.length === 0 || (searching && groups.length === 0)}
|
|
191
|
+
<Command.Empty forceMount class="p-3 text-kit-xs text-muted-foreground">
|
|
192
|
+
{t(models.length === 0 ? 'chat.picker.empty' : 'chat.picker.noMatch')}
|
|
193
|
+
</Command.Empty>
|
|
194
|
+
{/if}
|
|
195
|
+
{#each groups as group (group.id)}
|
|
196
|
+
{@const shown = isOpen(group.id)}
|
|
197
|
+
<Command.Group value={`group\n${group.id}`}>
|
|
198
|
+
<Command.GroupHeading>
|
|
199
|
+
<Button
|
|
200
|
+
variant="ghost"
|
|
201
|
+
size="xs"
|
|
202
|
+
class="w-full justify-start font-normal"
|
|
203
|
+
aria-expanded={shown}
|
|
204
|
+
aria-label={t('chat.picker.group', {
|
|
205
|
+
provider: group.provider,
|
|
206
|
+
state: t(shown ? 'chat.picker.expanded' : 'chat.picker.collapsed'),
|
|
207
|
+
})}
|
|
208
|
+
onclick={() => toggle(group.id)}
|
|
209
|
+
>
|
|
210
|
+
<span class="text-muted-foreground">{group.provider.charAt(0).toUpperCase()}</span
|
|
211
|
+
>
|
|
212
|
+
<span class="min-w-0 truncate">{group.provider}</span>
|
|
213
|
+
<span class="ml-auto max-w-32 truncate text-muted-foreground"
|
|
214
|
+
>{groupSummary(group, i18n)}</span
|
|
215
|
+
>
|
|
216
|
+
{#if shown}<ChevronDownIcon data-icon="inline-end" aria-hidden="true" />{:else}
|
|
217
|
+
<ChevronRightIcon data-icon="inline-end" aria-hidden="true" />{/if}
|
|
218
|
+
</Button>
|
|
219
|
+
</Command.GroupHeading>
|
|
220
|
+
{#if shown}
|
|
221
|
+
<Command.GroupItems>
|
|
222
|
+
{#each group.models as model (model.key)}
|
|
223
|
+
{@const selected = model.key === value?.key}
|
|
224
|
+
<Command.Item
|
|
225
|
+
value={model.key}
|
|
226
|
+
{disabled}
|
|
227
|
+
aria-current={selected ? 'true' : undefined}
|
|
228
|
+
aria-label={t('chat.picker.select', {
|
|
229
|
+
name: model.name,
|
|
230
|
+
provider: model.provider,
|
|
231
|
+
})}
|
|
232
|
+
onSelect={() => !disabled && onselect(nextValue(model, value, query))}
|
|
233
|
+
class={cn(
|
|
234
|
+
'flex cursor-pointer items-center gap-2 rounded-kit-sm py-1 pr-2 pl-6',
|
|
235
|
+
'data-[selected]:bg-accent data-[selected]:text-accent-foreground',
|
|
236
|
+
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
|
|
237
|
+
selected && 'bg-accent/60',
|
|
238
|
+
)}
|
|
239
|
+
>
|
|
240
|
+
<span class="flex min-w-0 flex-1 flex-col">
|
|
241
|
+
<span class="truncate">{model.name}</span>
|
|
242
|
+
<span
|
|
243
|
+
class="flex min-w-0 items-center gap-1 text-kit-xs text-muted-foreground"
|
|
244
|
+
>
|
|
245
|
+
{#if model.hasKey}<KeyIcon
|
|
246
|
+
class="size-3 shrink-0"
|
|
247
|
+
aria-hidden="true"
|
|
248
|
+
/>{/if}
|
|
249
|
+
<span class="truncate">{model.detail}</span>
|
|
250
|
+
</span>
|
|
251
|
+
</span>
|
|
252
|
+
{#if selected}<CheckIcon class="size-3 shrink-0" aria-hidden="true" />{/if}
|
|
253
|
+
</Command.Item>
|
|
254
|
+
{/each}
|
|
255
|
+
</Command.GroupItems>
|
|
256
|
+
{/if}
|
|
257
|
+
</Command.Group>
|
|
258
|
+
{/each}
|
|
259
|
+
</Command.List>
|
|
260
|
+
</Command.Root>
|
|
261
|
+
{#if current && current.efforts.length > 0}
|
|
262
|
+
<Separator />
|
|
263
|
+
<PickerEffort
|
|
264
|
+
model={current}
|
|
265
|
+
{effort}
|
|
266
|
+
{disabled}
|
|
267
|
+
{i18n}
|
|
268
|
+
onselect={(next) => onselect({ key: current.key, effort: next })}
|
|
269
|
+
/>
|
|
270
|
+
{:else if current}
|
|
271
|
+
<Separator />
|
|
272
|
+
<div class="flex items-center justify-between gap-2 px-2 py-1.5">
|
|
273
|
+
<p class="text-kit-xs text-muted-foreground">{t('chat.reasoningUnconfigured')}</p>
|
|
274
|
+
{#if onopensettings}
|
|
275
|
+
<Button
|
|
276
|
+
variant="outline"
|
|
277
|
+
size="xs"
|
|
278
|
+
onclick={() => {
|
|
279
|
+
open = false
|
|
280
|
+
onopensettings()
|
|
281
|
+
}}>{t('chat.openSettings')}</Button
|
|
282
|
+
>
|
|
283
|
+
{/if}
|
|
284
|
+
</div>
|
|
285
|
+
{/if}
|
|
286
|
+
{#if notice}
|
|
287
|
+
<p class="truncate px-2 pb-1 text-kit-xs text-muted-foreground" role="status">
|
|
288
|
+
{notice}
|
|
289
|
+
</p>
|
|
290
|
+
{/if}
|
|
291
|
+
<Separator />
|
|
292
|
+
<div class="flex items-center gap-0.5 p-1">
|
|
293
|
+
{#if onsetdefault}
|
|
294
|
+
<Button
|
|
295
|
+
variant="ghost"
|
|
296
|
+
size="xs"
|
|
297
|
+
disabled={disabled || value === null || sameAsDefault}
|
|
298
|
+
onclick={() => value && onsetdefault(value)}
|
|
299
|
+
>
|
|
300
|
+
<PinIcon data-icon="inline-start" aria-hidden="true" />{t('chat.picker.setDefault')}
|
|
301
|
+
</Button>
|
|
302
|
+
{/if}
|
|
303
|
+
<Button
|
|
304
|
+
variant="ghost"
|
|
305
|
+
size="xs"
|
|
306
|
+
disabled={disabled || !current?.efforts.includes('none')}
|
|
307
|
+
onclick={() => current && onselect({ key: current.key, effort: 'none' })}
|
|
308
|
+
>
|
|
309
|
+
<RefreshIcon data-icon="inline-start" aria-hidden="true" />{t('chat.picker.resetOff')}
|
|
310
|
+
</Button>
|
|
311
|
+
{#if onrefresh}
|
|
312
|
+
<Button
|
|
313
|
+
variant="outline"
|
|
314
|
+
size="xs"
|
|
315
|
+
class="ml-auto"
|
|
316
|
+
disabled={refreshing}
|
|
317
|
+
onclick={() => onrefresh(true)}
|
|
318
|
+
>
|
|
319
|
+
<RefreshIcon data-icon="inline-start" aria-hidden="true" />
|
|
320
|
+
{t(refreshing ? 'chat.picker.refreshing' : 'chat.picker.refresh')}
|
|
321
|
+
</Button>
|
|
322
|
+
{/if}
|
|
323
|
+
</div>
|
|
324
|
+
</Popover.Content>
|
|
325
|
+
</Popover.Portal>
|
|
326
|
+
</Popover.Root>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Effort section of the model picker: rail, level toggles, reset and hint (docs/31 §4.2).
|
|
3
|
+
import { ToggleGroup } from 'bits-ui'
|
|
4
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
5
|
+
import type { KitI18n } from '../i18n'
|
|
6
|
+
import type { ReasoningEffort } from '../ports'
|
|
7
|
+
import ReasoningSlider from './ReasoningSlider.svelte'
|
|
8
|
+
import type { PickerModel } from './model-effort'
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
model,
|
|
12
|
+
effort,
|
|
13
|
+
disabled = false,
|
|
14
|
+
i18n,
|
|
15
|
+
onselect,
|
|
16
|
+
}: {
|
|
17
|
+
model: PickerModel
|
|
18
|
+
effort: ReasoningEffort
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
i18n: KitI18n
|
|
21
|
+
onselect: (effort: ReasoningEffort) => void
|
|
22
|
+
} = $props()
|
|
23
|
+
const t = (key: string, params?: Record<string, string | number>) => i18n.t(key, params)
|
|
24
|
+
const label = $derived(t(`chat.effort.${effort}`))
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<section class="flex flex-col gap-0.5 px-2 pt-2 pb-1" aria-label={t('chat.reasoningEffort')}>
|
|
28
|
+
<div class="flex items-center justify-between gap-2">
|
|
29
|
+
<p class="text-kit-xs text-muted-foreground">
|
|
30
|
+
{t('chat.picker.effortFor', { name: model.name })}
|
|
31
|
+
</p>
|
|
32
|
+
{#if effort !== 'default'}
|
|
33
|
+
<Button variant="link" size="xs" {disabled} onclick={() => onselect('default')}>
|
|
34
|
+
{t('chat.resetReasoning')}
|
|
35
|
+
</Button>
|
|
36
|
+
{/if}
|
|
37
|
+
</div>
|
|
38
|
+
{#if model.efforts.length > 1}
|
|
39
|
+
<ReasoningSlider
|
|
40
|
+
efforts={model.efforts}
|
|
41
|
+
value={effort}
|
|
42
|
+
defaultIndex={Math.floor(model.efforts.length / 2)}
|
|
43
|
+
{disabled}
|
|
44
|
+
label={t('chat.reasoningEffort')}
|
|
45
|
+
valueText={label}
|
|
46
|
+
onchange={onselect}
|
|
47
|
+
/>
|
|
48
|
+
{/if}
|
|
49
|
+
<ToggleGroup.Root
|
|
50
|
+
type="single"
|
|
51
|
+
value={effort === 'default' ? '' : effort}
|
|
52
|
+
onValueChange={(next) => next && onselect(next as ReasoningEffort)}
|
|
53
|
+
{disabled}
|
|
54
|
+
aria-label={t('chat.reasoningEffort')}
|
|
55
|
+
class="flex"
|
|
56
|
+
>
|
|
57
|
+
{#each model.efforts as level (level)}
|
|
58
|
+
<ToggleGroup.Item
|
|
59
|
+
value={level}
|
|
60
|
+
aria-label={`${t('chat.reasoningEffort')} ${t(`chat.effort.${level}`)}`}
|
|
61
|
+
class="min-w-0 flex-1 rounded-kit-sm py-0.5 text-kit-xs whitespace-nowrap
|
|
62
|
+
text-muted-foreground hover:text-foreground focus-visible:outline-2
|
|
63
|
+
focus-visible:outline-ring disabled:cursor-not-allowed disabled:opacity-50
|
|
64
|
+
data-[state=on]:font-semibold data-[state=on]:text-foreground"
|
|
65
|
+
>
|
|
66
|
+
{t(`chat.effort.${level}`)}
|
|
67
|
+
</ToggleGroup.Item>
|
|
68
|
+
{/each}
|
|
69
|
+
</ToggleGroup.Root>
|
|
70
|
+
<p class="text-kit-xs text-muted-foreground">{t(`chat.effortHint.${effort}`)}</p>
|
|
71
|
+
</section>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { KitI18n } from '../i18n'
|
|
8
8
|
import type { ReasoningEffort } from '../ports'
|
|
9
9
|
import { REASONING_EFFORTS } from '../providers/reasoning'
|
|
10
|
+
import ReasoningSlider from './ReasoningSlider.svelte'
|
|
10
11
|
|
|
11
12
|
let {
|
|
12
13
|
value = 'default',
|
|
@@ -41,10 +42,6 @@
|
|
|
41
42
|
function choose(effort: ReasoningEffort) {
|
|
42
43
|
if (!disabled && (effort === 'default' || allowedEfforts.includes(effort))) onchange(effort)
|
|
43
44
|
}
|
|
44
|
-
function commit(event: Event & { currentTarget: HTMLInputElement }) {
|
|
45
|
-
const effort = allowedEfforts[Number(event.currentTarget.value)]
|
|
46
|
-
if (effort) choose(effort)
|
|
47
|
-
}
|
|
48
45
|
</script>
|
|
49
46
|
|
|
50
47
|
<Popover.Root bind:open>
|
|
@@ -105,45 +102,15 @@
|
|
|
105
102
|
<div class="reasoning-scale" aria-hidden="true">
|
|
106
103
|
<span>{t('chat.reasoningFaster')}</span><span>{t('chat.reasoningSmarter')}</span>
|
|
107
104
|
</div>
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
value={effortIndex}
|
|
118
|
-
aria-label={t('chat.reasoningEffort')}
|
|
119
|
-
aria-valuetext={effortLabel}
|
|
120
|
-
{disabled}
|
|
121
|
-
oninput={commit}
|
|
122
|
-
onpointerup={(event) => {
|
|
123
|
-
if (event.button === 0) commit(event)
|
|
124
|
-
}}
|
|
125
|
-
onkeyup={(event) => {
|
|
126
|
-
if (
|
|
127
|
-
!event.isComposing &&
|
|
128
|
-
['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(
|
|
129
|
-
event.key,
|
|
130
|
-
)
|
|
131
|
-
) {
|
|
132
|
-
commit(event)
|
|
133
|
-
}
|
|
134
|
-
}}
|
|
135
|
-
/>
|
|
136
|
-
<div class="reasoning-ticks" aria-hidden="true">
|
|
137
|
-
{#each allowedEfforts as effort, index (effort)}
|
|
138
|
-
<span
|
|
139
|
-
class="reasoning-tick"
|
|
140
|
-
data-effort={effort}
|
|
141
|
-
class:current={index === effortIndex}
|
|
142
|
-
style:left={`${(index / (allowedEfforts.length - 1)) * 100}%`}
|
|
143
|
-
></span>
|
|
144
|
-
{/each}
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
105
|
+
<ReasoningSlider
|
|
106
|
+
efforts={allowedEfforts}
|
|
107
|
+
value={effectiveEffort}
|
|
108
|
+
defaultIndex={effortIndex}
|
|
109
|
+
{disabled}
|
|
110
|
+
label={t('chat.reasoningEffort')}
|
|
111
|
+
valueText={effortLabel}
|
|
112
|
+
onchange={choose}
|
|
113
|
+
/>
|
|
147
114
|
{:else}
|
|
148
115
|
<Field.FieldSet {disabled}>
|
|
149
116
|
<Field.FieldLegend class="sr-only">{t('chat.reasoningEffort')}</Field.FieldLegend>
|
|
@@ -205,75 +172,6 @@
|
|
|
205
172
|
:global(.reasoning-popover input) {
|
|
206
173
|
accent-color: var(--color-primary);
|
|
207
174
|
}
|
|
208
|
-
.reasoning-slider {
|
|
209
|
-
position: relative;
|
|
210
|
-
height: 1.75rem;
|
|
211
|
-
display: flex;
|
|
212
|
-
align-items: center;
|
|
213
|
-
}
|
|
214
|
-
:global(.reasoning-slider input[type='range']) {
|
|
215
|
-
appearance: none;
|
|
216
|
-
width: 100%;
|
|
217
|
-
height: 1.75rem;
|
|
218
|
-
margin: 0;
|
|
219
|
-
background: transparent;
|
|
220
|
-
cursor: pointer;
|
|
221
|
-
}
|
|
222
|
-
:global(.reasoning-slider input::-webkit-slider-runnable-track) {
|
|
223
|
-
height: 0.375rem;
|
|
224
|
-
border-radius: 999px;
|
|
225
|
-
background: linear-gradient(
|
|
226
|
-
to right,
|
|
227
|
-
var(--color-primary) var(--reasoning-progress),
|
|
228
|
-
var(--color-muted) var(--reasoning-progress)
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
:global(.reasoning-slider input::-moz-range-track) {
|
|
232
|
-
height: 0.375rem;
|
|
233
|
-
border-radius: 999px;
|
|
234
|
-
background: linear-gradient(
|
|
235
|
-
to right,
|
|
236
|
-
var(--color-primary) var(--reasoning-progress),
|
|
237
|
-
var(--color-muted) var(--reasoning-progress)
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
:global(.reasoning-slider input::-webkit-slider-thumb) {
|
|
241
|
-
appearance: none;
|
|
242
|
-
width: 1.125rem;
|
|
243
|
-
height: 1.125rem;
|
|
244
|
-
margin-top: -0.375rem;
|
|
245
|
-
border: 1px solid var(--color-border);
|
|
246
|
-
border-radius: 50%;
|
|
247
|
-
background: var(--color-popover);
|
|
248
|
-
}
|
|
249
|
-
:global(.reasoning-slider input::-moz-range-thumb) {
|
|
250
|
-
width: 1.125rem;
|
|
251
|
-
height: 1.125rem;
|
|
252
|
-
box-sizing: border-box;
|
|
253
|
-
border: 1px solid var(--color-border);
|
|
254
|
-
border-radius: 50%;
|
|
255
|
-
background: var(--color-popover);
|
|
256
|
-
}
|
|
257
|
-
.reasoning-ticks {
|
|
258
|
-
position: absolute;
|
|
259
|
-
inset: 50% 0.5625rem auto;
|
|
260
|
-
pointer-events: none;
|
|
261
|
-
}
|
|
262
|
-
.reasoning-tick {
|
|
263
|
-
position: absolute;
|
|
264
|
-
width: 0.25rem;
|
|
265
|
-
height: 0.25rem;
|
|
266
|
-
border-radius: 50%;
|
|
267
|
-
background: var(--color-popover);
|
|
268
|
-
transform: translate(-50%, -50%);
|
|
269
|
-
}
|
|
270
|
-
.reasoning-tick.current {
|
|
271
|
-
visibility: hidden;
|
|
272
|
-
}
|
|
273
|
-
:global(.reasoning-slider input:disabled) {
|
|
274
|
-
cursor: not-allowed;
|
|
275
|
-
opacity: 0.5;
|
|
276
|
-
}
|
|
277
175
|
:global(.reasoning-popover input:focus-visible) {
|
|
278
176
|
outline: 2px solid var(--color-ring);
|
|
279
177
|
outline-offset: 2px;
|