@ouraihub/aether-ui 0.1.0 → 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 +3 -3
- package/src/chat/ApprovalCard.svelte +8 -2
- package/src/chat/ChatComposer.svelte +319 -0
- package/src/chat/ChatMessage.svelte +262 -0
- package/src/chat/ChatPanel.css +386 -0
- package/src/chat/ChatPanel.svelte +276 -1880
- package/src/chat/ChatPanelApprovals.svelte +28 -0
- package/src/chat/ChatPanelComposer.svelte +336 -0
- package/src/chat/ChatPanelDialogs.svelte +242 -0
- package/src/chat/ChatPanelHeader.svelte +108 -0
- package/src/chat/ChatPanelMessages.svelte +166 -0
- package/src/chat/ChatPanelMode.svelte +190 -0
- package/src/chat/ChatPanelState.svelte.ts +1 -0
- package/src/chat/ChatPanelToolbar.svelte +436 -0
- package/src/chat/ChatSurface.svelte +168 -0
- package/src/chat/ContextUsageStatus.svelte +97 -0
- package/src/chat/MessageItem.svelte +111 -214
- package/src/chat/ModelEffortPicker.svelte +326 -0
- package/src/chat/PickerEffort.svelte +71 -0
- package/src/chat/ReasoningControl.svelte +184 -0
- package/src/chat/ReasoningSlider.svelte +174 -0
- package/src/chat/SkillPicker.svelte +14 -6
- package/src/chat/UsageControl.svelte +223 -0
- package/src/chat/UsageDetails.svelte +44 -90
- package/src/chat/chat-attachments.svelte.ts +78 -0
- package/src/chat/chat-panel-history.svelte.ts +430 -0
- package/src/chat/chat-panel-models.svelte.ts +97 -0
- package/src/chat/chat-panel-props.ts +38 -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 +25 -1
- package/src/chat/model-effort.ts +177 -0
- package/src/chat/run-session-history.ts +56 -0
- package/src/chat/run-session-types.ts +69 -0
- package/src/chat/run-session.svelte.ts +73 -135
- package/src/chat/test-support/chat-fixture.ts +194 -0
- package/src/chat/test-support/conversation-fixtures.ts +32 -0
- package/src/chat/test-support/run-session-fixture.ts +112 -0
- package/src/dev/Demo.svelte +3 -1
- package/src/i18n/en-chat.d.ts +1 -0
- package/src/i18n/en-chat.js +201 -0
- package/src/i18n/en.ts +58 -180
- package/src/i18n/index.ts +7 -2
- 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 +8 -153
- package/src/logs/LogPanel.svelte +9 -3
- package/src/logs/LogSettings.svelte +3 -1
- package/src/mcp/McpClientEditor.svelte +204 -0
- package/src/mcp/McpClientSettings.svelte +57 -130
- package/src/mcp/McpServerSettings.svelte +47 -11
- package/src/mcp/input.ts +2 -1
- package/src/ports/contract.ts +11 -2
- package/src/ports/fake-optional.ts +81 -0
- package/src/ports/fake.ts +90 -86
- package/src/ports/search.ts +11 -2
- package/src/ports/test-support/fake-fixture.ts +63 -0
- package/src/providers/EndpointKeysDialog.svelte +27 -13
- package/src/providers/EndpointModels.svelte +311 -0
- package/src/providers/EndpointPanel.svelte +33 -258
- package/src/providers/ModelPickerDialog.svelte +59 -16
- package/src/providers/ModelPricing.svelte +125 -0
- package/src/providers/ModelSelect.svelte +22 -8
- package/src/providers/ModelSettingsDialog.svelte +61 -126
- package/src/providers/ProviderDialogs.svelte +120 -0
- package/src/providers/ProviderList.svelte +159 -0
- package/src/providers/ProvidersSettings.svelte +67 -207
- package/src/providers/RequestConfigDialog.svelte +40 -12
- package/src/providers/SecretInput.svelte +11 -3
- package/src/providers/draft.ts +39 -13
- package/src/providers/edit-session.ts +31 -7
- package/src/providers/reasoning.ts +18 -4
- package/src/providers/revision.svelte.ts +3 -1
- package/src/providers/test-support/providers-fixture.ts +120 -0
- package/src/search/SearchAdvancedTools.svelte +182 -0
- package/src/search/SearchProviderCard.svelte +195 -0
- package/src/search/SearchSettings.svelte +62 -265
- package/src/search/settings-flow.ts +8 -3
- package/src/skills/SkillAdd.svelte +51 -18
- package/src/skills/SkillAuthoring.svelte +69 -20
- package/src/skills/SkillEditor.svelte +13 -3
- package/src/skills/SkillPreviewDialog.svelte +19 -5
- package/src/skills/SkillsSettings.svelte +11 -4
- package/src/skills/test-support/skills-fixture.ts +139 -0
- package/src/tauri/index.ts +23 -7
- package/src/web/index.ts +90 -25
|
@@ -1,34 +1,21 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
// One endpoint's keys
|
|
2
|
+
// One endpoint's keys and address.
|
|
3
3
|
// Fields edit the draft in place; `oncommit` asks the parent to save (docs/33 §3); keys save on their own (docs/34).
|
|
4
|
-
import BrainIcon from '@lucide/svelte/icons/brain'
|
|
5
|
-
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
|
|
6
|
-
import ChevronsDownUpIcon from '@lucide/svelte/icons/chevrons-down-up'
|
|
7
|
-
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down'
|
|
8
|
-
import EyeIcon from '@lucide/svelte/icons/eye'
|
|
9
|
-
import Globe2Icon from '@lucide/svelte/icons/globe-2'
|
|
10
4
|
import LoaderIcon from '@lucide/svelte/icons/loader-circle'
|
|
11
|
-
import MinusIcon from '@lucide/svelte/icons/minus'
|
|
12
|
-
import PlusIcon from '@lucide/svelte/icons/plus'
|
|
13
|
-
import RefreshIcon from '@lucide/svelte/icons/refresh-cw'
|
|
14
|
-
import SearchIcon from '@lucide/svelte/icons/search'
|
|
15
5
|
import Settings2Icon from '@lucide/svelte/icons/settings-2'
|
|
16
|
-
import WrenchIcon from '@lucide/svelte/icons/wrench'
|
|
17
6
|
import { Button } from '#lib/components/ui/button/index.js'
|
|
18
7
|
import { Switch } from '#lib/components/ui/switch/index.js'
|
|
19
|
-
import * as Dialog from '#lib/components/ui/dialog/index.js'
|
|
20
8
|
import * as Field from '#lib/components/ui/field/index.js'
|
|
21
|
-
import { Input } from '#lib/components/ui/input/index.js'
|
|
22
9
|
import CopyInput from '../CopyInput.svelte'
|
|
23
10
|
import * as Select from '#lib/components/ui/select/index.js'
|
|
24
11
|
import { getKitContext } from '../context'
|
|
25
|
-
import type { KeyStrategy,
|
|
12
|
+
import type { KeyStrategy, Protocol, Provider } from '../ports'
|
|
26
13
|
import { cn } from '../utils'
|
|
27
|
-
import {
|
|
28
|
-
import ModelSettingsDialog from './ModelSettingsDialog.svelte'
|
|
14
|
+
import { keyError, maskedText, probeUrl, validUrl, type EndpointDraft } from './draft'
|
|
29
15
|
import RequestConfigDialog from './RequestConfigDialog.svelte'
|
|
30
16
|
import { settingsTarget } from './revision.svelte'
|
|
31
17
|
import SecretInput from './SecretInput.svelte'
|
|
18
|
+
import EndpointModels from './EndpointModels.svelte'
|
|
32
19
|
|
|
33
20
|
export type CheckOutcome =
|
|
34
21
|
{ state: 'ok'; count: number; suggested: Protocol | null } | { state: 'error'; message: string }
|
|
@@ -64,15 +51,6 @@
|
|
|
64
51
|
/** Localized roles bound to a stored model (contract §6). */
|
|
65
52
|
usedBy?: ((endpointId: string, modelId: string) => string[]) | undefined
|
|
66
53
|
} = $props()
|
|
67
|
-
|
|
68
|
-
const CAPS = ['vision', 'reasoning', 'tools', 'web'] as const
|
|
69
|
-
type Cap = (typeof CAPS)[number]
|
|
70
|
-
const CAP_STYLE: Record<Cap, string> = {
|
|
71
|
-
vision: 'bg-success/15 text-success',
|
|
72
|
-
reasoning: 'bg-info/15 text-info',
|
|
73
|
-
tools: 'bg-warning/15 text-warning',
|
|
74
|
-
web: 'bg-primary/10 text-primary',
|
|
75
|
-
}
|
|
76
54
|
const { ports, i18n, notify } = getKitContext()
|
|
77
55
|
const { t } = i18n
|
|
78
56
|
let keySaving = $state(false)
|
|
@@ -101,40 +79,12 @@
|
|
|
101
79
|
urlError = false
|
|
102
80
|
})
|
|
103
81
|
let settingsOpen = $state(false)
|
|
104
|
-
// A chat "open settings" action names this endpoint's model:
|
|
82
|
+
// A chat "open settings" action names this endpoint's model: consume it once.
|
|
105
83
|
const targeted = settingsTarget.value?.endpointId === endpoint.saved?.id
|
|
106
84
|
const targetModel = targeted
|
|
107
85
|
? (endpoint.models.find((model) => model.id === settingsTarget.value?.modelId) ?? null)
|
|
108
86
|
: null
|
|
109
87
|
if (targeted) settingsTarget.value = null
|
|
110
|
-
let modelOpen = $state(targetModel !== null)
|
|
111
|
-
let editing = $state<Model | null>(targetModel)
|
|
112
|
-
let addOpen = $state(false)
|
|
113
|
-
let newModelId = $state('')
|
|
114
|
-
let searching = $state(false)
|
|
115
|
-
let query = $state('')
|
|
116
|
-
let capFilter = $state<Cap | 'all'>('all')
|
|
117
|
-
let folded = $state<Record<string, boolean>>({})
|
|
118
|
-
|
|
119
|
-
const has = (model: Model, cap: Cap) =>
|
|
120
|
-
cap === 'vision'
|
|
121
|
-
? model.canVision
|
|
122
|
-
: cap === 'reasoning'
|
|
123
|
-
? model.reasoning !== null
|
|
124
|
-
: cap === 'tools'
|
|
125
|
-
? model.supportsTools === true
|
|
126
|
-
: model.nativeWebSearch
|
|
127
|
-
const shown = $derived.by(() => {
|
|
128
|
-
const search = query.trim().toLowerCase()
|
|
129
|
-
return endpoint.models.filter(
|
|
130
|
-
(model) =>
|
|
131
|
-
(model.id.toLowerCase().includes(search) || !!model.name?.toLowerCase().includes(search)) &&
|
|
132
|
-
(capFilter === 'all' || has(model, capFilter)),
|
|
133
|
-
)
|
|
134
|
-
})
|
|
135
|
-
const groups = $derived(Object.entries(Object.groupBy(shown, groupKey)))
|
|
136
|
-
const allFolded = $derived(groups.length > 0 && groups.every(([group]) => folded[group]))
|
|
137
|
-
|
|
138
88
|
function commitUrl() {
|
|
139
89
|
const next = url.trim()
|
|
140
90
|
if (next === endpoint.baseUrl) return
|
|
@@ -146,39 +96,12 @@
|
|
|
146
96
|
endpoint.baseUrl = next
|
|
147
97
|
oncommit()
|
|
148
98
|
}
|
|
149
|
-
function setModels(models: Model[]) {
|
|
150
|
-
endpoint.models = models
|
|
151
|
-
oncommit()
|
|
152
|
-
}
|
|
153
|
-
function addModel() {
|
|
154
|
-
const id = newModelId.trim()
|
|
155
|
-
if (id && !endpoint.models.some((model) => model.id === id)) {
|
|
156
|
-
setModels([
|
|
157
|
-
...endpoint.models,
|
|
158
|
-
{
|
|
159
|
-
id,
|
|
160
|
-
name: null,
|
|
161
|
-
groupName: null,
|
|
162
|
-
modelType: 'chat',
|
|
163
|
-
typeSource: 'user',
|
|
164
|
-
contextWindow: null,
|
|
165
|
-
maxOutput: null,
|
|
166
|
-
canVision: false,
|
|
167
|
-
supportsTools: null,
|
|
168
|
-
reasoning: null,
|
|
169
|
-
nativeWebSearch: false,
|
|
170
|
-
pricing: null,
|
|
171
|
-
},
|
|
172
|
-
])
|
|
173
|
-
}
|
|
174
|
-
newModelId = ''
|
|
175
|
-
addOpen = false
|
|
176
|
-
}
|
|
177
99
|
/** First probe suggestion: the whole check or any key's (all keys share one relay). */
|
|
178
100
|
const suggested = $derived.by((): Protocol | null => {
|
|
179
101
|
if (check?.state === 'ok') return check.suggested
|
|
180
102
|
if (check?.state !== 'keys') return null
|
|
181
|
-
for (const { value } of check.results)
|
|
103
|
+
for (const { value } of check.results)
|
|
104
|
+
if (value.state === 'ok' && value.suggested) return value.suggested
|
|
182
105
|
return null
|
|
183
106
|
})
|
|
184
107
|
</script>
|
|
@@ -196,7 +119,12 @@
|
|
|
196
119
|
</div>
|
|
197
120
|
<div class="flex min-w-0 items-start gap-2">
|
|
198
121
|
<SecretInput id="kit-endpoint-key" endpoint={endpoint.saved} onchanged={onkeys} />
|
|
199
|
-
<Button
|
|
122
|
+
<Button
|
|
123
|
+
variant="outline"
|
|
124
|
+
class="shrink-0"
|
|
125
|
+
disabled={!validUrl(endpoint.baseUrl)}
|
|
126
|
+
onclick={oncheck}
|
|
127
|
+
>
|
|
200
128
|
{#if check?.state === 'running'}<LoaderIcon class="animate-spin" />{/if}
|
|
201
129
|
{t('providers.check')}
|
|
202
130
|
</Button>
|
|
@@ -219,7 +147,9 @@
|
|
|
219
147
|
>
|
|
220
148
|
<Switch
|
|
221
149
|
size="sm"
|
|
222
|
-
aria-label={t('providers.keys_enable_for', {
|
|
150
|
+
aria-label={t('providers.keys_enable_for', {
|
|
151
|
+
name: key.label || maskedText(key, t),
|
|
152
|
+
})}
|
|
223
153
|
checked={key.enabled}
|
|
224
154
|
disabled={keySaving}
|
|
225
155
|
onCheckedChange={(enabled) => void setKeyEnabled(key.id, enabled)}
|
|
@@ -231,14 +161,20 @@
|
|
|
231
161
|
{:else if check && check.state !== 'running'}
|
|
232
162
|
<p
|
|
233
163
|
role="status"
|
|
234
|
-
class={cn(
|
|
164
|
+
class={cn(
|
|
165
|
+
'text-kit-xs whitespace-pre-line',
|
|
166
|
+
check.state === 'ok' ? 'text-success' : 'text-destructive',
|
|
167
|
+
)}
|
|
235
168
|
>
|
|
236
169
|
{check.state === 'ok' ? t('providers.check_ok', { count: check.count }) : check.message}
|
|
237
170
|
</p>
|
|
238
171
|
{/if}
|
|
239
172
|
{#if suggested && suggested !== endpoint.protocol}
|
|
240
173
|
{@const next = suggested}
|
|
241
|
-
<div
|
|
174
|
+
<div
|
|
175
|
+
role="status"
|
|
176
|
+
class="bg-info/10 flex min-w-0 items-center gap-2 rounded-md px-2.5 py-1.5 text-kit-xs"
|
|
177
|
+
>
|
|
242
178
|
<span class="min-w-0 flex-1">{t('providers.suggest_protocol')}</span>
|
|
243
179
|
<Button
|
|
244
180
|
variant="outline"
|
|
@@ -287,7 +223,11 @@
|
|
|
287
223
|
<div class="flex items-center gap-2">
|
|
288
224
|
<Field.Label for="kit-endpoint-url">{t('providers.base_url')}</Field.Label>
|
|
289
225
|
{#if onaddendpoint}
|
|
290
|
-
<button
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
class="text-primary text-kit-xs hover:underline"
|
|
229
|
+
onclick={onaddendpoint}
|
|
230
|
+
>
|
|
291
231
|
{t('providers.add_endpoint')}
|
|
292
232
|
</button>
|
|
293
233
|
{/if}
|
|
@@ -314,178 +254,13 @@
|
|
|
314
254
|
{#if urlError}
|
|
315
255
|
<p role="alert" class="text-destructive text-kit-xs">{t('providers.url_invalid')}</p>
|
|
316
256
|
{:else if validUrl(endpoint.baseUrl)}
|
|
317
|
-
<p class="text-muted-foreground truncate font-mono text-kit-xs">
|
|
257
|
+
<p class="text-muted-foreground truncate font-mono text-kit-xs">
|
|
258
|
+
GET {probeUrl(endpoint.baseUrl)}
|
|
259
|
+
</p>
|
|
318
260
|
{/if}
|
|
319
261
|
</Field.Field>
|
|
320
262
|
|
|
321
|
-
<
|
|
322
|
-
<div class="flex min-w-0 flex-wrap items-center gap-1">
|
|
323
|
-
<h3 id="kit-endpoint-models" class="mr-1 text-kit-base font-semibold">{t('providers.models')}</h3>
|
|
324
|
-
<Button
|
|
325
|
-
variant="ghost"
|
|
326
|
-
size="icon-xs"
|
|
327
|
-
title={t(allFolded ? 'providers.expand_all' : 'providers.collapse_all')}
|
|
328
|
-
aria-label={t(allFolded ? 'providers.expand_all' : 'providers.collapse_all')}
|
|
329
|
-
disabled={groups.length === 0}
|
|
330
|
-
onclick={() => (folded = allFolded ? {} : Object.fromEntries(groups.map(([group]) => [group, true])))}
|
|
331
|
-
>{#if allFolded}<ChevronsUpDownIcon />{:else}<ChevronsDownUpIcon />{/if}</Button
|
|
332
|
-
>
|
|
333
|
-
{#if searching || query}
|
|
334
|
-
<Input
|
|
335
|
-
class="h-7 w-36 text-kit-sm"
|
|
336
|
-
autofocus
|
|
337
|
-
placeholder={t('providers.model_search')}
|
|
338
|
-
aria-label={t('providers.model_search')}
|
|
339
|
-
bind:value={query}
|
|
340
|
-
onblur={() => (searching = false)}
|
|
341
|
-
onkeydown={(event) => event.key === 'Escape' && ((query = ''), event.currentTarget.blur())}
|
|
342
|
-
/>
|
|
343
|
-
{:else}
|
|
344
|
-
<Button
|
|
345
|
-
variant="ghost"
|
|
346
|
-
size="icon-xs"
|
|
347
|
-
title={t('providers.model_search')}
|
|
348
|
-
aria-label={t('providers.model_search')}
|
|
349
|
-
onclick={() => (searching = true)}><SearchIcon /></Button
|
|
350
|
-
>
|
|
351
|
-
{/if}
|
|
352
|
-
<Select.Root type="single" value={capFilter} onValueChange={(value) => (capFilter = value as Cap | 'all')}>
|
|
353
|
-
<Select.Trigger
|
|
354
|
-
size="sm"
|
|
355
|
-
class={cn('border-0 shadow-none', capFilter !== 'all' && 'bg-accent')}
|
|
356
|
-
aria-label={t('providers.cap_filter')}
|
|
357
|
-
>
|
|
358
|
-
{capFilter === 'all' ? t('providers.all_types') : t(`providers.cap_${capFilter}`)}
|
|
359
|
-
</Select.Trigger>
|
|
360
|
-
<Select.Content>
|
|
361
|
-
<Select.Group>
|
|
362
|
-
<Select.Item value="all" label={t('providers.all_types')} />
|
|
363
|
-
{#each CAPS as cap (cap)}<Select.Item value={cap} label={t(`providers.cap_${cap}`)} />{/each}
|
|
364
|
-
</Select.Group>
|
|
365
|
-
</Select.Content>
|
|
366
|
-
</Select.Root>
|
|
367
|
-
<div class="ml-auto flex items-center gap-1">
|
|
368
|
-
<Button variant="outline" size="sm" disabled={!validUrl(endpoint.baseUrl)} onclick={onsync}>
|
|
369
|
-
<RefreshIcon />{t('providers.sync_models')}
|
|
370
|
-
</Button>
|
|
371
|
-
<Button
|
|
372
|
-
variant="outline"
|
|
373
|
-
size="icon-sm"
|
|
374
|
-
title={t('providers.add_model_manual')}
|
|
375
|
-
aria-label={t('providers.add_model_manual')}
|
|
376
|
-
onclick={() => (addOpen = true)}><PlusIcon /></Button
|
|
377
|
-
>
|
|
378
|
-
</div>
|
|
379
|
-
</div>
|
|
380
|
-
|
|
381
|
-
{#each groups as [group, models = []] (group)}
|
|
382
|
-
<section class="group/model-group overflow-hidden rounded-kit border">
|
|
383
|
-
<div class="bg-muted/30 flex items-center gap-1 pr-2">
|
|
384
|
-
<button
|
|
385
|
-
type="button"
|
|
386
|
-
class="flex min-w-0 flex-1 items-center gap-2 px-3 py-2 text-left text-kit-sm"
|
|
387
|
-
aria-expanded={!folded[group]}
|
|
388
|
-
onclick={() => (folded[group] = !folded[group])}
|
|
389
|
-
>
|
|
390
|
-
<ChevronRightIcon class={cn('size-4 shrink-0 transition-transform', !folded[group] && 'rotate-90')} />
|
|
391
|
-
<span class="truncate">{group || t('providers.no_group')}</span>
|
|
392
|
-
<span class="text-muted-foreground text-kit-xs tabular-nums">{models.length}</span>
|
|
393
|
-
</button>
|
|
394
|
-
<Button
|
|
395
|
-
variant="ghost"
|
|
396
|
-
size="icon-xs"
|
|
397
|
-
class="opacity-0 group-hover/model-group:opacity-100 focus-visible:opacity-100"
|
|
398
|
-
title={t('providers.remove_group')}
|
|
399
|
-
aria-label={t('providers.remove_group')}
|
|
400
|
-
onclick={() => setModels(endpoint.models.filter((model) => !models.includes(model)))}><MinusIcon /></Button
|
|
401
|
-
>
|
|
402
|
-
</div>
|
|
403
|
-
{#if !folded[group]}
|
|
404
|
-
<ul class="divide-y border-t">
|
|
405
|
-
{#each models as model (model.id)}
|
|
406
|
-
<li class="hover:bg-accent/40 flex min-w-0 items-center gap-2 px-3 py-1.5">
|
|
407
|
-
<span
|
|
408
|
-
aria-hidden="true"
|
|
409
|
-
class="bg-muted text-muted-foreground flex size-6 shrink-0 items-center justify-center rounded-full text-kit-xs uppercase"
|
|
410
|
-
>{(model.name ?? model.id).slice(0, 1)}</span
|
|
411
|
-
>
|
|
412
|
-
<span class="min-w-0 flex-1 truncate text-kit-sm" title={model.id}>{model.name ?? model.id}</span>
|
|
413
|
-
<span class="flex shrink-0 items-center gap-0.5">
|
|
414
|
-
{#each CAPS.filter((cap) => has(model, cap)) as cap (cap)}
|
|
415
|
-
<span
|
|
416
|
-
class={cn('flex h-5 w-6 items-center justify-center rounded-full', CAP_STYLE[cap])}
|
|
417
|
-
title={t(`providers.cap_${cap}`)}
|
|
418
|
-
>
|
|
419
|
-
{#if cap === 'vision'}<EyeIcon class="size-3" />{:else if cap === 'reasoning'}<BrainIcon
|
|
420
|
-
class="size-3"
|
|
421
|
-
/>{:else if cap === 'tools'}<WrenchIcon class="size-3" />{:else}<Globe2Icon
|
|
422
|
-
class="size-3"
|
|
423
|
-
/>{/if}
|
|
424
|
-
<span class="sr-only">{t(`providers.cap_${cap}`)}</span>
|
|
425
|
-
</span>
|
|
426
|
-
{/each}
|
|
427
|
-
</span>
|
|
428
|
-
<Button
|
|
429
|
-
variant="ghost"
|
|
430
|
-
size="icon-xs"
|
|
431
|
-
title={t('providers.model_settings')}
|
|
432
|
-
aria-label={t('providers.model_settings_for', { id: model.id })}
|
|
433
|
-
onclick={() => {
|
|
434
|
-
editing = model
|
|
435
|
-
modelOpen = true
|
|
436
|
-
}}><Settings2Icon /></Button
|
|
437
|
-
>
|
|
438
|
-
<Button
|
|
439
|
-
variant="ghost"
|
|
440
|
-
size="icon-xs"
|
|
441
|
-
title={t('providers.remove_model', { id: model.id })}
|
|
442
|
-
aria-label={t('providers.remove_model', { id: model.id })}
|
|
443
|
-
onclick={() => setModels(endpoint.models.filter((item) => item.id !== model.id))}
|
|
444
|
-
><MinusIcon /></Button
|
|
445
|
-
>
|
|
446
|
-
</li>
|
|
447
|
-
{/each}
|
|
448
|
-
</ul>
|
|
449
|
-
{/if}
|
|
450
|
-
</section>
|
|
451
|
-
{:else}
|
|
452
|
-
<p class="text-muted-foreground rounded-kit border border-dashed px-3 py-6 text-center text-kit-sm">
|
|
453
|
-
{endpoint.models.length === 0 ? t('providers.no_models') : t('providers.no_model_matches')}
|
|
454
|
-
</p>
|
|
455
|
-
{/each}
|
|
456
|
-
</section>
|
|
263
|
+
<EndpointModels {endpoint} {onsync} {oncommit} {usedBy} {targetModel} />
|
|
457
264
|
</div>
|
|
458
265
|
|
|
459
|
-
<ModelSettingsDialog
|
|
460
|
-
bind:open={modelOpen}
|
|
461
|
-
model={editing}
|
|
462
|
-
protocol={endpoint.protocol}
|
|
463
|
-
usedBy={editing && endpoint.saved && usedBy ? usedBy(endpoint.saved.id, editing.id) : []}
|
|
464
|
-
onsave={(saved) => setModels(endpoint.models.map((model) => (model.id === saved.id ? saved : model)))}
|
|
465
|
-
/>
|
|
466
|
-
|
|
467
266
|
<RequestConfigDialog bind:open={settingsOpen} bind:endpoint {onaddendpoint} {onremove} {oncommit} />
|
|
468
|
-
|
|
469
|
-
<Dialog.Root bind:open={addOpen}>
|
|
470
|
-
<Dialog.Content class="sm:max-w-md">
|
|
471
|
-
<Dialog.Header>
|
|
472
|
-
<Dialog.Title>{t('providers.add_model_manual')}</Dialog.Title>
|
|
473
|
-
</Dialog.Header>
|
|
474
|
-
<form
|
|
475
|
-
class="contents"
|
|
476
|
-
onsubmit={(event) => {
|
|
477
|
-
event.preventDefault()
|
|
478
|
-
addModel()
|
|
479
|
-
}}
|
|
480
|
-
>
|
|
481
|
-
<Field.Field>
|
|
482
|
-
<Field.Label for="kit-model-new">{t('providers.model_id')}</Field.Label>
|
|
483
|
-
<Input id="kit-model-new" class="font-mono" autocomplete="off" spellcheck={false} bind:value={newModelId} />
|
|
484
|
-
</Field.Field>
|
|
485
|
-
<Dialog.Footer>
|
|
486
|
-
<Button type="button" variant="outline" onclick={() => (addOpen = false)}>{t('common.cancel')}</Button>
|
|
487
|
-
<Button type="submit" disabled={!newModelId.trim()}>{t('common.confirm')}</Button>
|
|
488
|
-
</Dialog.Footer>
|
|
489
|
-
</form>
|
|
490
|
-
</Dialog.Content>
|
|
491
|
-
</Dialog.Root>
|
|
@@ -26,7 +26,16 @@
|
|
|
26
26
|
onchange: (models: Model[]) => void
|
|
27
27
|
} = $props()
|
|
28
28
|
|
|
29
|
-
const TYPES: ModelType[] = [
|
|
29
|
+
const TYPES: ModelType[] = [
|
|
30
|
+
'chat',
|
|
31
|
+
'image',
|
|
32
|
+
'video',
|
|
33
|
+
'tts',
|
|
34
|
+
'stt',
|
|
35
|
+
'embedding',
|
|
36
|
+
'rerank',
|
|
37
|
+
'unknown',
|
|
38
|
+
]
|
|
30
39
|
const { t } = getKitContext().i18n
|
|
31
40
|
|
|
32
41
|
let checked = $state<Record<string, boolean>>({})
|
|
@@ -39,15 +48,21 @@
|
|
|
39
48
|
if (!open || !result) return
|
|
40
49
|
const current = Object.fromEntries(untrack(() => selected).map((m) => [m.id, m]))
|
|
41
50
|
checked = Object.fromEntries(result.models.map((m) => [m.id, m.id in current]))
|
|
42
|
-
types = Object.fromEntries(
|
|
51
|
+
types = Object.fromEntries(
|
|
52
|
+
result.models.map((m) => [m.id, current[m.id]?.modelType ?? m.modelType]),
|
|
53
|
+
)
|
|
43
54
|
query = ''
|
|
44
55
|
filter = 'all'
|
|
45
56
|
collapsed = {}
|
|
46
57
|
})
|
|
47
58
|
|
|
48
59
|
const models = $derived(result?.models ?? [])
|
|
49
|
-
const searched = $derived(
|
|
50
|
-
|
|
60
|
+
const searched = $derived(
|
|
61
|
+
models.filter((m) => m.id.toLowerCase().includes(query.trim().toLowerCase())),
|
|
62
|
+
)
|
|
63
|
+
const shown = $derived(
|
|
64
|
+
searched.filter((m) => filter === 'all' || (types[m.id] ?? m.modelType) === filter),
|
|
65
|
+
)
|
|
51
66
|
const groups = $derived(Object.entries(Object.groupBy(shown, (m) => m.group ?? '')))
|
|
52
67
|
const count = $derived(models.filter((m) => checked[m.id]).length)
|
|
53
68
|
const bulkRemoves = $derived(shown.length > 0 && shown.every((m) => checked[m.id]))
|
|
@@ -77,7 +92,9 @@
|
|
|
77
92
|
<Dialog.Header class="shrink-0 text-left">
|
|
78
93
|
<Dialog.Title class="flex items-center gap-2 pr-6">
|
|
79
94
|
{t('providers.picker_title')}
|
|
80
|
-
<span aria-hidden="true" class="bg-muted rounded px-1.5 text-kit-xs tabular-nums"
|
|
95
|
+
<span aria-hidden="true" class="bg-muted rounded px-1.5 text-kit-xs tabular-nums"
|
|
96
|
+
>{models.length}</span
|
|
97
|
+
>
|
|
81
98
|
</Dialog.Title>
|
|
82
99
|
<Dialog.Description>{t('providers.picker_hint')}</Dialog.Description>
|
|
83
100
|
</Dialog.Header>
|
|
@@ -90,14 +107,26 @@
|
|
|
90
107
|
autocomplete="off"
|
|
91
108
|
spellcheck={false}
|
|
92
109
|
/>
|
|
93
|
-
<Button
|
|
110
|
+
<Button
|
|
111
|
+
variant="outline"
|
|
112
|
+
size="sm"
|
|
113
|
+
class="shrink-0"
|
|
114
|
+
disabled={shown.length === 0}
|
|
115
|
+
onclick={bulk}
|
|
116
|
+
>
|
|
94
117
|
{t(bulkRemoves ? 'providers.remove_all' : 'providers.add_all')}
|
|
95
118
|
</Button>
|
|
96
119
|
</div>
|
|
97
|
-
<div
|
|
120
|
+
<div
|
|
121
|
+
class="flex shrink-0 gap-1 overflow-x-auto pb-1"
|
|
122
|
+
role="group"
|
|
123
|
+
aria-label={t('providers.model_type')}
|
|
124
|
+
>
|
|
98
125
|
{#each ['all', ...TYPES] as type (type)}
|
|
99
126
|
{@const hits =
|
|
100
|
-
type === 'all'
|
|
127
|
+
type === 'all'
|
|
128
|
+
? searched.length
|
|
129
|
+
: searched.filter((m) => (types[m.id] ?? m.modelType) === type).length}
|
|
101
130
|
<Button
|
|
102
131
|
variant={filter === type ? 'secondary' : 'ghost'}
|
|
103
132
|
size="sm"
|
|
@@ -111,9 +140,15 @@
|
|
|
111
140
|
</Button>
|
|
112
141
|
{/each}
|
|
113
142
|
</div>
|
|
114
|
-
<div
|
|
143
|
+
<div
|
|
144
|
+
class="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto"
|
|
145
|
+
role="region"
|
|
146
|
+
aria-label={t('providers.models')}
|
|
147
|
+
>
|
|
115
148
|
{#if shown.length === 0}
|
|
116
|
-
<p class="text-muted-foreground py-8 text-center text-kit-sm">
|
|
149
|
+
<p class="text-muted-foreground py-8 text-center text-kit-sm">
|
|
150
|
+
{t('providers.no_model_matches')}
|
|
151
|
+
</p>
|
|
117
152
|
{/if}
|
|
118
153
|
{#each groups as [group, items = []] (group)}
|
|
119
154
|
{@const folded = !query.trim() && collapsed[group]}
|
|
@@ -132,8 +167,12 @@
|
|
|
132
167
|
{#if !folded}
|
|
133
168
|
<ul>
|
|
134
169
|
{#each items as model (model.id)}
|
|
135
|
-
<li
|
|
136
|
-
|
|
170
|
+
<li
|
|
171
|
+
class="hover:bg-accent/50 flex min-w-0 items-center gap-1 rounded-md py-0.5 pl-4"
|
|
172
|
+
>
|
|
173
|
+
<span class="min-w-0 flex-1 truncate font-mono text-kit-sm" title={model.id}
|
|
174
|
+
>{model.id}</span
|
|
175
|
+
>
|
|
137
176
|
<Select.Root
|
|
138
177
|
type="single"
|
|
139
178
|
value={types[model.id] ?? model.modelType}
|
|
@@ -154,7 +193,8 @@
|
|
|
154
193
|
</Select.Group></Select.Content
|
|
155
194
|
>
|
|
156
195
|
</Select.Root>
|
|
157
|
-
{#if model.canVision}<span
|
|
196
|
+
{#if model.canVision}<span
|
|
197
|
+
class="text-muted-foreground hidden shrink-0 text-kit-xs sm:inline"
|
|
158
198
|
>{t('providers.vision')}</span
|
|
159
199
|
>{/if}
|
|
160
200
|
<span class="text-success w-8 shrink-0 text-right text-kit-xs"
|
|
@@ -164,9 +204,12 @@
|
|
|
164
204
|
variant="ghost"
|
|
165
205
|
size="icon-sm"
|
|
166
206
|
class="shrink-0"
|
|
167
|
-
aria-label={t(
|
|
168
|
-
id:
|
|
169
|
-
|
|
207
|
+
aria-label={t(
|
|
208
|
+
checked[model.id] ? 'providers.remove_model' : 'providers.add_model',
|
|
209
|
+
{
|
|
210
|
+
id: model.id,
|
|
211
|
+
},
|
|
212
|
+
)}
|
|
170
213
|
onclick={() => toggle(model.id)}
|
|
171
214
|
>
|
|
172
215
|
{#if checked[model.id]}<Minus />{:else}<Plus />{/if}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getKitContext } from '../context'
|
|
3
|
+
import MinusIcon from '@lucide/svelte/icons/minus'
|
|
4
|
+
import PlusIcon from '@lucide/svelte/icons/plus'
|
|
5
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
6
|
+
import * as Field from '#lib/components/ui/field/index.js'
|
|
7
|
+
import { Input } from '#lib/components/ui/input/index.js'
|
|
8
|
+
import type { Model, PricingTier } from '../ports'
|
|
9
|
+
import type { ModelField } from './draft'
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
edit,
|
|
13
|
+
errorFor,
|
|
14
|
+
}: { edit: Model | null; errorFor: (field: ModelField) => { message: string }[] } = $props()
|
|
15
|
+
const { t } = getKitContext().i18n
|
|
16
|
+
const PRICES: (keyof PricingTier)[] = ['input', 'output', 'cacheRead', 'cacheWrite']
|
|
17
|
+
const CURRENCIES = [
|
|
18
|
+
['USD', '$'],
|
|
19
|
+
['CNY', '¥'],
|
|
20
|
+
] as const
|
|
21
|
+
const CHIP = 'aria-pressed:border-primary aria-pressed:bg-primary/10 aria-pressed:text-primary'
|
|
22
|
+
const count = (value: number) => (Number.isNaN(value) ? null : value)
|
|
23
|
+
const tier = (minInputTokens: number): PricingTier => ({
|
|
24
|
+
minInputTokens,
|
|
25
|
+
input: 0,
|
|
26
|
+
output: 0,
|
|
27
|
+
cacheRead: null,
|
|
28
|
+
cacheWrite: null,
|
|
29
|
+
})
|
|
30
|
+
function setPrice(index: number, key: keyof PricingTier, value: number) {
|
|
31
|
+
const target = edit?.pricing?.tiers[index]
|
|
32
|
+
if (!target) return
|
|
33
|
+
if (key === 'cacheRead' || key === 'cacheWrite') target[key] = count(value)
|
|
34
|
+
else target[key] = value
|
|
35
|
+
}
|
|
36
|
+
function removeTier(index: number) {
|
|
37
|
+
const pricing = edit?.pricing
|
|
38
|
+
if (!edit || !pricing) return
|
|
39
|
+
pricing.tiers.splice(index, 1)
|
|
40
|
+
if (pricing.tiers[0]) pricing.tiers[0].minInputTokens = 0
|
|
41
|
+
else edit.pricing = null
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<Field.Set>
|
|
46
|
+
<Field.Legend variant="label">{t('providers.pricing')}</Field.Legend>
|
|
47
|
+
{#if edit?.pricing}
|
|
48
|
+
{@const current = edit.pricing}
|
|
49
|
+
<div class="flex flex-wrap gap-1.5" role="group" aria-label={t('providers.currency')}>
|
|
50
|
+
{#each CURRENCIES as [currency, symbol] (currency)}
|
|
51
|
+
<Button
|
|
52
|
+
variant="outline"
|
|
53
|
+
size="sm"
|
|
54
|
+
class={CHIP}
|
|
55
|
+
aria-pressed={current.currency === currency}
|
|
56
|
+
aria-label={t(`providers.currency.${currency}`)}
|
|
57
|
+
onclick={() => (current.currency = currency)}>{symbol}</Button
|
|
58
|
+
>
|
|
59
|
+
{/each}
|
|
60
|
+
</div>
|
|
61
|
+
<p class="text-muted-foreground text-kit-xs">{t('providers.pricing_unit')}</p>
|
|
62
|
+
{#each current.tiers as item, index (index)}
|
|
63
|
+
<fieldset class="flex flex-col gap-2 rounded-kit border p-2">
|
|
64
|
+
<legend class="flex w-full items-center gap-2 px-1 text-kit-sm"
|
|
65
|
+
>{t('providers.tier', { n: index + 1 })}{#if current.tiers.length > 1}<Button
|
|
66
|
+
variant="ghost"
|
|
67
|
+
size="icon-xs"
|
|
68
|
+
class="ml-auto"
|
|
69
|
+
aria-label={t('providers.tier_remove', { n: index + 1 })}
|
|
70
|
+
title={t('providers.tier_remove', { n: index + 1 })}
|
|
71
|
+
onclick={() => removeTier(index)}><MinusIcon /></Button
|
|
72
|
+
>{/if}</legend
|
|
73
|
+
>
|
|
74
|
+
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
|
75
|
+
<label class="flex flex-col gap-1 text-kit-xs"
|
|
76
|
+
>{t('providers.tier_start')}<Input
|
|
77
|
+
type="number"
|
|
78
|
+
min="0"
|
|
79
|
+
step="1"
|
|
80
|
+
disabled={index === 0}
|
|
81
|
+
value={item.minInputTokens}
|
|
82
|
+
oninput={(event) =>
|
|
83
|
+
setPrice(index, 'minInputTokens', event.currentTarget.valueAsNumber)}
|
|
84
|
+
/></label
|
|
85
|
+
>
|
|
86
|
+
{#each PRICES as key (key)}
|
|
87
|
+
<label class="flex flex-col gap-1 text-kit-xs"
|
|
88
|
+
>{t(`providers.price.${key}`)}<Input
|
|
89
|
+
type="number"
|
|
90
|
+
min="0"
|
|
91
|
+
step="any"
|
|
92
|
+
placeholder={key === 'cacheRead' || key === 'cacheWrite'
|
|
93
|
+
? t('providers.price_same_as_input')
|
|
94
|
+
: ''}
|
|
95
|
+
value={item[key] ?? ''}
|
|
96
|
+
oninput={(event) => setPrice(index, key, event.currentTarget.valueAsNumber)}
|
|
97
|
+
/></label
|
|
98
|
+
>
|
|
99
|
+
{/each}
|
|
100
|
+
</div>
|
|
101
|
+
</fieldset>
|
|
102
|
+
{/each}
|
|
103
|
+
<Field.Error errors={errorFor('pricing')} />
|
|
104
|
+
<div class="flex flex-wrap gap-1.5">
|
|
105
|
+
{#if current.tiers.length < 8}<Button
|
|
106
|
+
variant="outline"
|
|
107
|
+
size="sm"
|
|
108
|
+
onclick={() =>
|
|
109
|
+
current.tiers.push(tier((current.tiers.at(-1)?.minInputTokens ?? 0) + 200000))}
|
|
110
|
+
><PlusIcon />{t('providers.tier_add')}</Button
|
|
111
|
+
>{/if}
|
|
112
|
+
<Button variant="ghost" size="sm" onclick={() => edit && (edit.pricing = null)}
|
|
113
|
+
>{t('providers.pricing_clear')}</Button
|
|
114
|
+
>
|
|
115
|
+
</div>
|
|
116
|
+
{:else}
|
|
117
|
+
<Button
|
|
118
|
+
variant="outline"
|
|
119
|
+
size="sm"
|
|
120
|
+
class="self-start"
|
|
121
|
+
onclick={() => edit && (edit.pricing = { currency: 'USD', tiers: [tier(0)] })}
|
|
122
|
+
><PlusIcon />{t('providers.pricing_add')}</Button
|
|
123
|
+
>
|
|
124
|
+
{/if}
|
|
125
|
+
</Field.Set>
|