@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
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
|
|
51
51
|
const key = (r: ModelRef) => `${r.endpointId}\n${r.modelId}`
|
|
52
52
|
// label is `{provider} · {slug} · {modelId}`; a model's display name never enters it.
|
|
53
|
-
const group = (o: ModelOption) =>
|
|
53
|
+
const group = (o: ModelOption) =>
|
|
54
|
+
o.label.slice(0, Math.max(0, o.label.lastIndexOf(' · '))) || o.label
|
|
54
55
|
type Cap = 'vision' | 'reasoning' | 'tools' | 'web'
|
|
55
56
|
const CAPS = [
|
|
56
57
|
{ cap: 'vision', icon: EyeIcon, has: (o: ModelOption) => o.canVision },
|
|
@@ -105,7 +106,9 @@
|
|
|
105
106
|
{#if failed}
|
|
106
107
|
<div class="model-error" role="alert">
|
|
107
108
|
<span>{t('chat.modelsFailed')}</span>
|
|
108
|
-
<Button variant="outline" size="sm" {disabled} onclick={() => reload++}
|
|
109
|
+
<Button variant="outline" size="sm" {disabled} onclick={() => reload++}
|
|
110
|
+
>{t('chat.retry')}</Button
|
|
111
|
+
>
|
|
109
112
|
</div>
|
|
110
113
|
{:else}
|
|
111
114
|
<Popover.Root
|
|
@@ -132,7 +135,9 @@
|
|
|
132
135
|
<span class="model-group">| {group(current)}</span>
|
|
133
136
|
{:else}
|
|
134
137
|
<span class="model-name muted"
|
|
135
|
-
>{invalid
|
|
138
|
+
>{invalid
|
|
139
|
+
? t('providers.model_invalid')
|
|
140
|
+
: (placeholder ?? t('providers.model_placeholder'))}</span
|
|
136
141
|
>
|
|
137
142
|
{/if}
|
|
138
143
|
<ChevronDownIcon class="model-chevron" />
|
|
@@ -150,7 +155,11 @@
|
|
|
150
155
|
if (event.isComposing) event.preventDefault()
|
|
151
156
|
}}
|
|
152
157
|
>
|
|
153
|
-
<Command.Root
|
|
158
|
+
<Command.Root
|
|
159
|
+
bind:value={highlighted}
|
|
160
|
+
filter={match}
|
|
161
|
+
label={t('providers.model_placeholder')}
|
|
162
|
+
>
|
|
154
163
|
<div class="model-search">
|
|
155
164
|
<SearchIcon class="model-search-icon" />
|
|
156
165
|
<Command.Input class="model-search-input" placeholder={t('providers.model_search')} />
|
|
@@ -163,8 +172,9 @@
|
|
|
163
172
|
class="cap-chip cap-{c.cap}"
|
|
164
173
|
aria-pressed={active.includes(c.cap)}
|
|
165
174
|
onclick={() =>
|
|
166
|
-
(active = active.includes(c.cap)
|
|
167
|
-
|
|
175
|
+
(active = active.includes(c.cap)
|
|
176
|
+
? active.filter((x) => x !== c.cap)
|
|
177
|
+
: [...active, c.cap])}><c.icon />{t(`providers.cap_${c.cap}`)}</button
|
|
168
178
|
>
|
|
169
179
|
{/each}
|
|
170
180
|
</div>
|
|
@@ -180,13 +190,17 @@
|
|
|
180
190
|
class="model-item"
|
|
181
191
|
value={key(option)}
|
|
182
192
|
keywords={[option.modelId, option.name ?? '', name]}
|
|
183
|
-
aria-current={value !== null && key(option) === key(value)
|
|
193
|
+
aria-current={value !== null && key(option) === key(value)
|
|
194
|
+
? 'true'
|
|
195
|
+
: undefined}
|
|
184
196
|
onSelect={() => pick(option)}
|
|
185
197
|
>
|
|
186
198
|
<span class="model-avatar" aria-hidden="true"
|
|
187
199
|
>{(option.name ?? option.modelId).charAt(0).toUpperCase()}</span
|
|
188
200
|
>
|
|
189
|
-
<span class="model-name" title={option.label}
|
|
201
|
+
<span class="model-name" title={option.label}
|
|
202
|
+
>{option.name ?? option.modelId}</span
|
|
203
|
+
>
|
|
190
204
|
<span class="model-tags">
|
|
191
205
|
{#each CAPS.filter((c) => c.has(option)) as c (c.cap)}
|
|
192
206
|
<span class="cap-tag cap-{c.cap}" title={t(`providers.cap_${c.cap}`)}
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// Per-model settings (docs/41 §3–§4). Edits a copy; nothing changes until Save.
|
|
3
3
|
import { untrack } from 'svelte'
|
|
4
4
|
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
|
|
5
|
-
import
|
|
6
|
-
import PlusIcon from '@lucide/svelte/icons/plus'
|
|
5
|
+
import ModelPricing from './ModelPricing.svelte'
|
|
7
6
|
import { Button } from '#lib/components/ui/button/index.js'
|
|
8
7
|
import * as Dialog from '#lib/components/ui/dialog/index.js'
|
|
9
8
|
import * as Field from '#lib/components/ui/field/index.js'
|
|
@@ -11,10 +10,24 @@
|
|
|
11
10
|
import * as Select from '#lib/components/ui/select/index.js'
|
|
12
11
|
import CopyInput from '../CopyInput.svelte'
|
|
13
12
|
import { getKitContext } from '../context'
|
|
14
|
-
import type { Model, ModelType,
|
|
13
|
+
import type { Model, ModelType, Protocol, ReasoningDialect, ReasoningEffort } from '../ports'
|
|
15
14
|
import { cn } from '../utils'
|
|
16
|
-
import {
|
|
17
|
-
|
|
15
|
+
import {
|
|
16
|
+
idPrefix,
|
|
17
|
+
purposeOf,
|
|
18
|
+
validateModel,
|
|
19
|
+
withPurpose,
|
|
20
|
+
withType,
|
|
21
|
+
type ModelField,
|
|
22
|
+
type Purpose,
|
|
23
|
+
} from './draft'
|
|
24
|
+
import {
|
|
25
|
+
REASONING_DIALECTS,
|
|
26
|
+
REASONING_EFFORTS,
|
|
27
|
+
allowsAuto,
|
|
28
|
+
dialectFits,
|
|
29
|
+
emptyReasoning,
|
|
30
|
+
} from './reasoning'
|
|
18
31
|
|
|
19
32
|
let {
|
|
20
33
|
open = $bindable(false),
|
|
@@ -48,12 +61,7 @@
|
|
|
48
61
|
['64K', 65536],
|
|
49
62
|
['128K', 131072],
|
|
50
63
|
]
|
|
51
|
-
const PRICES = ['input', 'output', 'cacheRead', 'cacheWrite'] as const
|
|
52
64
|
const PURPOSES: Purpose[] = ['chat', 'image', 'edit']
|
|
53
|
-
const CURRENCIES = [
|
|
54
|
-
['USD', '$'],
|
|
55
|
-
['CNY', '¥'],
|
|
56
|
-
] as const
|
|
57
65
|
const CHIP = 'aria-pressed:border-primary aria-pressed:bg-primary/10 aria-pressed:text-primary'
|
|
58
66
|
|
|
59
67
|
const { t } = getKitContext().i18n
|
|
@@ -90,28 +98,8 @@
|
|
|
90
98
|
}
|
|
91
99
|
/** Tri-state: not probed → supported → unsupported → not probed. */
|
|
92
100
|
function cycleTools() {
|
|
93
|
-
if (edit)
|
|
94
|
-
|
|
95
|
-
const tier = (minInputTokens: number): PricingTier => ({
|
|
96
|
-
minInputTokens,
|
|
97
|
-
input: 0,
|
|
98
|
-
output: 0,
|
|
99
|
-
cacheRead: null,
|
|
100
|
-
cacheWrite: null,
|
|
101
|
-
})
|
|
102
|
-
function setPrice(index: number, key: keyof PricingTier, value: number) {
|
|
103
|
-
const target = edit?.pricing?.tiers[index]
|
|
104
|
-
if (!target) return
|
|
105
|
-
// Required prices stay NaN when cleared so validation flags them; cache prices fall back to the input price.
|
|
106
|
-
if (key === 'cacheRead' || key === 'cacheWrite') target[key] = count(value)
|
|
107
|
-
else target[key] = value
|
|
108
|
-
}
|
|
109
|
-
function removeTier(index: number) {
|
|
110
|
-
const pricing = edit?.pricing
|
|
111
|
-
if (!edit || !pricing) return
|
|
112
|
-
pricing.tiers.splice(index, 1)
|
|
113
|
-
if (pricing.tiers[0]) pricing.tiers[0].minInputTokens = 0
|
|
114
|
-
else edit.pricing = null
|
|
101
|
+
if (edit)
|
|
102
|
+
edit.supportsTools = edit.supportsTools === null ? true : edit.supportsTools ? false : null
|
|
115
103
|
}
|
|
116
104
|
function save() {
|
|
117
105
|
if (!edit || errors.length > 0) return
|
|
@@ -192,17 +180,22 @@
|
|
|
192
180
|
size="sm"
|
|
193
181
|
class={CHIP}
|
|
194
182
|
aria-pressed={edit.modelType === type}
|
|
195
|
-
onclick={() => edit && (edit = withType(edit, type))}
|
|
183
|
+
onclick={() => edit && (edit = withType(edit, type))}
|
|
184
|
+
>{t(`providers.type.${type}`)}</Button
|
|
196
185
|
>
|
|
197
186
|
{/each}
|
|
198
187
|
<Select.Root
|
|
199
188
|
type="single"
|
|
200
189
|
value={OTHER_TYPES.includes(edit.modelType) ? edit.modelType : ''}
|
|
201
|
-
onValueChange={(value) =>
|
|
190
|
+
onValueChange={(value) =>
|
|
191
|
+
edit && value && (edit = withType(edit, value as ModelType))}
|
|
202
192
|
>
|
|
203
193
|
<Select.Trigger
|
|
204
194
|
size="sm"
|
|
205
|
-
class={cn(
|
|
195
|
+
class={cn(
|
|
196
|
+
OTHER_TYPES.includes(edit.modelType) &&
|
|
197
|
+
'border-primary bg-primary/10 text-primary',
|
|
198
|
+
)}
|
|
206
199
|
aria-label={t('providers.type_other')}
|
|
207
200
|
>{OTHER_TYPES.includes(edit.modelType)
|
|
208
201
|
? t(`providers.type.${edit.modelType}`)
|
|
@@ -220,7 +213,9 @@
|
|
|
220
213
|
</div>
|
|
221
214
|
{#if usedBy.length > 0 && edit.modelType !== model?.modelType}
|
|
222
215
|
<Field.Description class="text-warning text-kit-xs" role="status"
|
|
223
|
-
>{t('providers.type_in_use', {
|
|
216
|
+
>{t('providers.type_in_use', {
|
|
217
|
+
roles: usedBy.join(t('providers.list_separator')),
|
|
218
|
+
})}</Field.Description
|
|
224
219
|
>
|
|
225
220
|
{/if}
|
|
226
221
|
</Field.Set>
|
|
@@ -234,18 +229,25 @@
|
|
|
234
229
|
class={CHIP}
|
|
235
230
|
aria-pressed={edit.reasoning !== null}
|
|
236
231
|
onclick={() =>
|
|
237
|
-
edit &&
|
|
232
|
+
edit &&
|
|
233
|
+
(edit.reasoning = edit.reasoning
|
|
234
|
+
? null
|
|
235
|
+
: emptyReasoning(dialects[0] ?? 'openai'))}
|
|
238
236
|
>{t('providers.cap_reasoning')}</Button
|
|
239
237
|
>
|
|
240
238
|
<Button
|
|
241
239
|
variant="outline"
|
|
242
240
|
size="sm"
|
|
243
|
-
class={cn(
|
|
241
|
+
class={cn(
|
|
242
|
+
CHIP,
|
|
243
|
+
edit.supportsTools === false && 'text-muted-foreground line-through',
|
|
244
|
+
)}
|
|
244
245
|
aria-pressed={edit.supportsTools === true}
|
|
245
246
|
title={t(`providers.tools_state.${edit.supportsTools ?? 'unknown'}`)}
|
|
246
247
|
onclick={cycleTools}
|
|
247
248
|
>{t('providers.cap_tools')}{#if edit.supportsTools === null}<span
|
|
248
|
-
class="text-muted-foreground text-kit-xs"
|
|
249
|
+
class="text-muted-foreground text-kit-xs"
|
|
250
|
+
>{t('providers.tools_state.unknown')}</span
|
|
249
251
|
>{/if}</Button
|
|
250
252
|
>
|
|
251
253
|
{#if protocol !== 'openai-chat'}
|
|
@@ -272,7 +274,8 @@
|
|
|
272
274
|
size="sm"
|
|
273
275
|
class={CHIP}
|
|
274
276
|
aria-pressed={edit.canVision}
|
|
275
|
-
onclick={() => edit && (edit.canVision = !edit.canVision)}
|
|
277
|
+
onclick={() => edit && (edit.canVision = !edit.canVision)}
|
|
278
|
+
>{t('providers.cap_vision')}</Button
|
|
276
279
|
>
|
|
277
280
|
</div>
|
|
278
281
|
</Field.Set>
|
|
@@ -280,7 +283,7 @@
|
|
|
280
283
|
{@render tokens('contextWindow', CONTEXT_PRESETS, 32000)}
|
|
281
284
|
{@render tokens('maxOutput', OUTPUT_PRESETS, 4096)}
|
|
282
285
|
{#if edit.modelType === 'chat'}
|
|
283
|
-
{
|
|
286
|
+
<ModelPricing {edit} {errorFor} />
|
|
284
287
|
{/if}
|
|
285
288
|
{/if}
|
|
286
289
|
</Field.Group>
|
|
@@ -299,12 +302,18 @@
|
|
|
299
302
|
<Select.Root
|
|
300
303
|
type="single"
|
|
301
304
|
value={dialect}
|
|
302
|
-
onValueChange={(value) =>
|
|
305
|
+
onValueChange={(value) =>
|
|
306
|
+
edit && (edit.reasoning = emptyReasoning(value as ReasoningDialect))}
|
|
303
307
|
>
|
|
304
|
-
<Select.Trigger id="kit-model-dialect" class="w-full"
|
|
308
|
+
<Select.Trigger id="kit-model-dialect" class="w-full"
|
|
309
|
+
>{t(`providers.dialect.${dialect}`)}</Select.Trigger
|
|
310
|
+
>
|
|
305
311
|
<Select.Content>
|
|
306
312
|
<Select.Group>
|
|
307
|
-
{#each dialects as item (item)}<Select.Item
|
|
313
|
+
{#each dialects as item (item)}<Select.Item
|
|
314
|
+
value={item}
|
|
315
|
+
label={t(`providers.dialect.${item}`)}
|
|
316
|
+
/>{/each}
|
|
308
317
|
</Select.Group>
|
|
309
318
|
</Select.Content>
|
|
310
319
|
</Select.Root>
|
|
@@ -322,7 +331,9 @@
|
|
|
322
331
|
>
|
|
323
332
|
{/each}
|
|
324
333
|
</div>
|
|
325
|
-
{#if dialect !== 'anthropic-budget' && dialect !== 'qwen'}<Field.Error
|
|
334
|
+
{#if dialect !== 'anthropic-budget' && dialect !== 'qwen'}<Field.Error
|
|
335
|
+
errors={errorFor('reasoning')}
|
|
336
|
+
/>{/if}
|
|
326
337
|
</Field.Set>
|
|
327
338
|
{#if dialect === 'anthropic-budget' || dialect === 'qwen'}
|
|
328
339
|
<Field.Field>
|
|
@@ -341,7 +352,11 @@
|
|
|
341
352
|
{/if}
|
|
342
353
|
{/snippet}
|
|
343
354
|
|
|
344
|
-
{#snippet tokens(
|
|
355
|
+
{#snippet tokens(
|
|
356
|
+
field: 'contextWindow' | 'maxOutput',
|
|
357
|
+
presets: [string, number][],
|
|
358
|
+
fallback: number,
|
|
359
|
+
)}
|
|
345
360
|
{@const value = edit?.[field] ?? null}
|
|
346
361
|
<Field.Field>
|
|
347
362
|
<Field.Label for="kit-model-{field}"
|
|
@@ -371,83 +386,3 @@
|
|
|
371
386
|
<Field.Error errors={errorFor(field)} />
|
|
372
387
|
</Field.Field>
|
|
373
388
|
{/snippet}
|
|
374
|
-
|
|
375
|
-
{#snippet pricing()}
|
|
376
|
-
<Field.Set>
|
|
377
|
-
<Field.Legend variant="label">{t('providers.pricing')}</Field.Legend>
|
|
378
|
-
{#if edit?.pricing}
|
|
379
|
-
{@const current = edit.pricing}
|
|
380
|
-
<div class="flex flex-wrap gap-1.5" role="group" aria-label={t('providers.currency')}>
|
|
381
|
-
{#each CURRENCIES as [currency, symbol] (currency)}
|
|
382
|
-
<Button
|
|
383
|
-
variant="outline"
|
|
384
|
-
size="sm"
|
|
385
|
-
class={CHIP}
|
|
386
|
-
aria-pressed={current.currency === currency}
|
|
387
|
-
aria-label={t(`providers.currency.${currency}`)}
|
|
388
|
-
onclick={() => (current.currency = currency)}>{symbol}</Button
|
|
389
|
-
>
|
|
390
|
-
{/each}
|
|
391
|
-
</div>
|
|
392
|
-
<p class="text-muted-foreground text-kit-xs">{t('providers.pricing_unit')}</p>
|
|
393
|
-
{#each current.tiers as item, index (index)}
|
|
394
|
-
<fieldset class="flex flex-col gap-2 rounded-kit border p-2">
|
|
395
|
-
<legend class="flex w-full items-center gap-2 px-1 text-kit-sm"
|
|
396
|
-
>{t('providers.tier', { n: index + 1 })}{#if current.tiers.length > 1}<Button
|
|
397
|
-
variant="ghost"
|
|
398
|
-
size="icon-xs"
|
|
399
|
-
class="ml-auto"
|
|
400
|
-
aria-label={t('providers.tier_remove', { n: index + 1 })}
|
|
401
|
-
title={t('providers.tier_remove', { n: index + 1 })}
|
|
402
|
-
onclick={() => removeTier(index)}><MinusIcon /></Button
|
|
403
|
-
>{/if}</legend
|
|
404
|
-
>
|
|
405
|
-
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
|
406
|
-
<label class="flex flex-col gap-1 text-kit-xs"
|
|
407
|
-
>{t('providers.tier_start')}<Input
|
|
408
|
-
type="number"
|
|
409
|
-
min="0"
|
|
410
|
-
step="1"
|
|
411
|
-
disabled={index === 0}
|
|
412
|
-
value={item.minInputTokens}
|
|
413
|
-
oninput={(event) => setPrice(index, 'minInputTokens', event.currentTarget.valueAsNumber)}
|
|
414
|
-
/></label
|
|
415
|
-
>
|
|
416
|
-
{#each PRICES as key (key)}
|
|
417
|
-
<label class="flex flex-col gap-1 text-kit-xs"
|
|
418
|
-
>{t(`providers.price.${key}`)}<Input
|
|
419
|
-
type="number"
|
|
420
|
-
min="0"
|
|
421
|
-
step="any"
|
|
422
|
-
placeholder={key === 'cacheRead' || key === 'cacheWrite' ? t('providers.price_same_as_input') : ''}
|
|
423
|
-
value={item[key] ?? ''}
|
|
424
|
-
oninput={(event) => setPrice(index, key, event.currentTarget.valueAsNumber)}
|
|
425
|
-
/></label
|
|
426
|
-
>
|
|
427
|
-
{/each}
|
|
428
|
-
</div>
|
|
429
|
-
</fieldset>
|
|
430
|
-
{/each}
|
|
431
|
-
<Field.Error errors={errorFor('pricing')} />
|
|
432
|
-
<div class="flex flex-wrap gap-1.5">
|
|
433
|
-
{#if current.tiers.length < 8}<Button
|
|
434
|
-
variant="outline"
|
|
435
|
-
size="sm"
|
|
436
|
-
onclick={() => current.tiers.push(tier((current.tiers.at(-1)?.minInputTokens ?? 0) + 200000))}
|
|
437
|
-
><PlusIcon />{t('providers.tier_add')}</Button
|
|
438
|
-
>{/if}
|
|
439
|
-
<Button variant="ghost" size="sm" onclick={() => edit && (edit.pricing = null)}
|
|
440
|
-
>{t('providers.pricing_clear')}</Button
|
|
441
|
-
>
|
|
442
|
-
</div>
|
|
443
|
-
{:else}
|
|
444
|
-
<Button
|
|
445
|
-
variant="outline"
|
|
446
|
-
size="sm"
|
|
447
|
-
class="self-start"
|
|
448
|
-
onclick={() => edit && (edit.pricing = { currency: 'USD', tiers: [tier(0)] })}
|
|
449
|
-
><PlusIcon />{t('providers.pricing_add')}</Button
|
|
450
|
-
>
|
|
451
|
-
{/if}
|
|
452
|
-
</Field.Set>
|
|
453
|
-
{/snippet}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Dialog from '#lib/components/ui/dialog/index.js'
|
|
3
|
+
import * as Field from '#lib/components/ui/field/index.js'
|
|
4
|
+
import { Input } from '#lib/components/ui/input/index.js'
|
|
5
|
+
import { Textarea } from '#lib/components/ui/textarea/index.js'
|
|
6
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
7
|
+
import type { Provider } from '../ports'
|
|
8
|
+
import type { ProviderDraft } from './draft'
|
|
9
|
+
import { getKitContext } from '../context'
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
addOpen = $bindable(false),
|
|
13
|
+
infoOpen = $bindable(false),
|
|
14
|
+
notesOpen = $bindable(false),
|
|
15
|
+
newName = $bindable(''),
|
|
16
|
+
editName = $bindable(''),
|
|
17
|
+
editNotes = $bindable(''),
|
|
18
|
+
draft,
|
|
19
|
+
providers,
|
|
20
|
+
create,
|
|
21
|
+
rename,
|
|
22
|
+
saveNotes,
|
|
23
|
+
remove,
|
|
24
|
+
}: {
|
|
25
|
+
addOpen?: boolean
|
|
26
|
+
infoOpen?: boolean
|
|
27
|
+
notesOpen?: boolean
|
|
28
|
+
newName?: string
|
|
29
|
+
editName?: string
|
|
30
|
+
editNotes?: string
|
|
31
|
+
draft: ProviderDraft | null
|
|
32
|
+
providers: Provider[]
|
|
33
|
+
create: () => void | Promise<void>
|
|
34
|
+
rename: () => void
|
|
35
|
+
saveNotes: () => void
|
|
36
|
+
remove: () => void | Promise<void>
|
|
37
|
+
} = $props()
|
|
38
|
+
const { i18n } = getKitContext()
|
|
39
|
+
const { t } = i18n
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<Dialog.Root bind:open={addOpen}>
|
|
43
|
+
<Dialog.Content class="sm:max-w-md">
|
|
44
|
+
<Dialog.Header><Dialog.Title>{t('providers.add')}</Dialog.Title></Dialog.Header>
|
|
45
|
+
<form
|
|
46
|
+
class="contents"
|
|
47
|
+
onsubmit={(event) => {
|
|
48
|
+
event.preventDefault()
|
|
49
|
+
void create()
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<Field.Field>
|
|
53
|
+
<Field.Label for="kit-provider-new">{t('providers.name')}</Field.Label>
|
|
54
|
+
<Input id="kit-provider-new" maxlength={80} bind:value={newName} />
|
|
55
|
+
</Field.Field>
|
|
56
|
+
<Dialog.Footer>
|
|
57
|
+
<Button type="button" variant="outline" onclick={() => (addOpen = false)}
|
|
58
|
+
>{t('common.cancel')}</Button
|
|
59
|
+
>
|
|
60
|
+
<Button type="submit" disabled={!newName.trim()}>{t('common.confirm')}</Button>
|
|
61
|
+
</Dialog.Footer>
|
|
62
|
+
</form>
|
|
63
|
+
</Dialog.Content>
|
|
64
|
+
</Dialog.Root>
|
|
65
|
+
|
|
66
|
+
<Dialog.Root bind:open={infoOpen}>
|
|
67
|
+
<Dialog.Content class="sm:max-w-md">
|
|
68
|
+
<Dialog.Header><Dialog.Title>{t('providers.settings')}</Dialog.Title></Dialog.Header>
|
|
69
|
+
<form
|
|
70
|
+
class="contents"
|
|
71
|
+
onsubmit={(event) => {
|
|
72
|
+
event.preventDefault()
|
|
73
|
+
rename()
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<Field.Field>
|
|
77
|
+
<Field.Label for="kit-provider-name">{t('providers.name')}</Field.Label>
|
|
78
|
+
<Input id="kit-provider-name" maxlength={80} bind:value={editName} />
|
|
79
|
+
</Field.Field>
|
|
80
|
+
<Dialog.Footer class="sm:justify-between">
|
|
81
|
+
{#if !providers.find((p) => p.id === draft?.id)?.presetId}
|
|
82
|
+
<Button
|
|
83
|
+
type="button"
|
|
84
|
+
variant="ghost"
|
|
85
|
+
class="text-destructive"
|
|
86
|
+
onclick={() => void remove()}>{t('providers.delete')}</Button
|
|
87
|
+
>
|
|
88
|
+
{:else}<span></span>{/if}
|
|
89
|
+
<Button type="submit" disabled={!editName.trim()}>{t('providers.save')}</Button>
|
|
90
|
+
</Dialog.Footer>
|
|
91
|
+
</form>
|
|
92
|
+
</Dialog.Content>
|
|
93
|
+
</Dialog.Root>
|
|
94
|
+
|
|
95
|
+
<Dialog.Root bind:open={notesOpen}>
|
|
96
|
+
<Dialog.Content class="sm:max-w-lg">
|
|
97
|
+
<Dialog.Header><Dialog.Title>{t('providers.notes')}</Dialog.Title></Dialog.Header>
|
|
98
|
+
<form
|
|
99
|
+
class="contents"
|
|
100
|
+
onsubmit={(event) => {
|
|
101
|
+
event.preventDefault()
|
|
102
|
+
saveNotes()
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<Textarea
|
|
106
|
+
class="min-h-64 font-mono text-kit-sm"
|
|
107
|
+
aria-label={t('providers.notes')}
|
|
108
|
+
placeholder={t('providers.notes_placeholder')}
|
|
109
|
+
maxlength={10000}
|
|
110
|
+
bind:value={editNotes}
|
|
111
|
+
/>
|
|
112
|
+
<Dialog.Footer>
|
|
113
|
+
<Button type="button" variant="outline" onclick={() => (notesOpen = false)}
|
|
114
|
+
>{t('common.cancel')}</Button
|
|
115
|
+
>
|
|
116
|
+
<Button type="submit">{t('providers.save')}</Button>
|
|
117
|
+
</Dialog.Footer>
|
|
118
|
+
</form>
|
|
119
|
+
</Dialog.Content>
|
|
120
|
+
</Dialog.Root>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import EllipsisIcon from '@lucide/svelte/icons/ellipsis-vertical'
|
|
3
|
+
import PlusIcon from '@lucide/svelte/icons/plus'
|
|
4
|
+
import SearchIcon from '@lucide/svelte/icons/search'
|
|
5
|
+
import SquarePenIcon from '@lucide/svelte/icons/square-pen'
|
|
6
|
+
import SlidersHorizontalIcon from '@lucide/svelte/icons/sliders-horizontal'
|
|
7
|
+
import Trash2Icon from '@lucide/svelte/icons/trash-2'
|
|
8
|
+
import UserPenIcon from '@lucide/svelte/icons/user-pen'
|
|
9
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
10
|
+
import * as DropdownMenu from '#lib/components/ui/dropdown-menu/index.js'
|
|
11
|
+
import { Input } from '#lib/components/ui/input/index.js'
|
|
12
|
+
import * as Select from '#lib/components/ui/select/index.js'
|
|
13
|
+
import { getKitContext } from '../context'
|
|
14
|
+
import type { Provider } from '../ports'
|
|
15
|
+
import { cn } from '../utils'
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
providers,
|
|
19
|
+
selectedId,
|
|
20
|
+
onselect,
|
|
21
|
+
onaction,
|
|
22
|
+
onadd,
|
|
23
|
+
}: {
|
|
24
|
+
providers: Provider[]
|
|
25
|
+
selectedId: string | null
|
|
26
|
+
onselect: (provider: Provider) => void
|
|
27
|
+
onaction: (provider: Provider, action: 'edit' | 'notes' | 'delete') => void
|
|
28
|
+
onadd: () => void
|
|
29
|
+
} = $props()
|
|
30
|
+
const { i18n } = getKitContext()
|
|
31
|
+
const { t } = i18n
|
|
32
|
+
const FILTERS = ['all', 'enabled', 'disabled'] as const
|
|
33
|
+
let query = $state('')
|
|
34
|
+
let filter = $state<(typeof FILTERS)[number]>('all')
|
|
35
|
+
let menuFor = $state<string | null>(null)
|
|
36
|
+
const visible = $derived(
|
|
37
|
+
providers.filter(
|
|
38
|
+
(p) =>
|
|
39
|
+
p.name.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()) &&
|
|
40
|
+
(filter === 'all' || (filter === 'enabled') === p.enabled),
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
function status(p: Provider): string | null {
|
|
44
|
+
if (!p.enabled) return null
|
|
45
|
+
return p.endpoints.length === 0 ||
|
|
46
|
+
p.endpoints.some((e) => !e.keys.some((k) => k.enabled && k.readable))
|
|
47
|
+
? 'bg-warning'
|
|
48
|
+
: 'bg-success'
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<nav class="flex w-full shrink-0 flex-col gap-1 sm:w-60" aria-label={t('providers.title')}>
|
|
53
|
+
<div class="mb-1 flex items-center gap-1.5">
|
|
54
|
+
<div class="relative min-w-0 flex-1">
|
|
55
|
+
<SearchIcon
|
|
56
|
+
class="text-muted-foreground pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2"
|
|
57
|
+
/>
|
|
58
|
+
<Input
|
|
59
|
+
class="pl-8"
|
|
60
|
+
placeholder={t('providers.search')}
|
|
61
|
+
aria-label={t('providers.search')}
|
|
62
|
+
bind:value={query}
|
|
63
|
+
autocomplete="off"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
<Select.Root
|
|
67
|
+
type="single"
|
|
68
|
+
value={filter}
|
|
69
|
+
onValueChange={(value) => (filter = value as typeof filter)}
|
|
70
|
+
>
|
|
71
|
+
<Select.Trigger
|
|
72
|
+
class={cn('shrink-0 px-2 [&>svg:last-child]:hidden', filter !== 'all' && 'bg-accent')}
|
|
73
|
+
aria-label={t('providers.filter')}
|
|
74
|
+
title={t(`providers.filter_${filter}`)}
|
|
75
|
+
>
|
|
76
|
+
<SlidersHorizontalIcon />
|
|
77
|
+
</Select.Trigger>
|
|
78
|
+
<Select.Content
|
|
79
|
+
><Select.Group
|
|
80
|
+
>{#each FILTERS as f (f)}<Select.Item
|
|
81
|
+
value={f}
|
|
82
|
+
label={t(`providers.filter_${f}`)}
|
|
83
|
+
/>{/each}</Select.Group
|
|
84
|
+
></Select.Content
|
|
85
|
+
>
|
|
86
|
+
</Select.Root>
|
|
87
|
+
</div>
|
|
88
|
+
<ul class="flex min-w-0 flex-col gap-0.5">
|
|
89
|
+
{#each visible as p (p.id)}
|
|
90
|
+
{@const dot = status(p)}
|
|
91
|
+
<li
|
|
92
|
+
class="group/row relative"
|
|
93
|
+
oncontextmenu={(event) => {
|
|
94
|
+
event.preventDefault()
|
|
95
|
+
menuFor = p.id
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class={cn(
|
|
101
|
+
'hover:bg-muted flex h-9 w-full min-w-0 items-center gap-2.5 rounded-kit px-2 pr-8 text-left text-kit-base',
|
|
102
|
+
selectedId === p.id && 'bg-muted font-medium',
|
|
103
|
+
)}
|
|
104
|
+
aria-current={selectedId === p.id ? 'true' : undefined}
|
|
105
|
+
onclick={() => onselect(p)}
|
|
106
|
+
>
|
|
107
|
+
<span
|
|
108
|
+
aria-hidden="true"
|
|
109
|
+
class="bg-primary/10 text-primary flex size-6 shrink-0 items-center justify-center rounded-md text-kit-xs font-semibold"
|
|
110
|
+
>{p.name.slice(0, 1).toUpperCase()}</span
|
|
111
|
+
>
|
|
112
|
+
<span class={cn('min-w-0 flex-1 truncate', !p.enabled && 'text-muted-foreground')}
|
|
113
|
+
>{p.name}</span
|
|
114
|
+
>
|
|
115
|
+
{#if dot}<span class={cn('size-1.5 shrink-0 rounded-full', dot)}></span><span
|
|
116
|
+
class="sr-only"
|
|
117
|
+
>{t(
|
|
118
|
+
dot === 'bg-success' ? 'providers.filter_enabled' : 'providers.needs_setup',
|
|
119
|
+
)}</span
|
|
120
|
+
>{/if}
|
|
121
|
+
</button>
|
|
122
|
+
<DropdownMenu.Root
|
|
123
|
+
open={menuFor === p.id}
|
|
124
|
+
onOpenChange={(open) => (menuFor = open ? p.id : null)}
|
|
125
|
+
>
|
|
126
|
+
<DropdownMenu.Trigger
|
|
127
|
+
>{#snippet child({ props })}<button
|
|
128
|
+
{...props}
|
|
129
|
+
type="button"
|
|
130
|
+
class={cn(
|
|
131
|
+
'text-muted-foreground hover:text-foreground hover:bg-accent absolute top-1/2 right-1 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md opacity-0 group-hover/row:opacity-100 focus-visible:opacity-100',
|
|
132
|
+
menuFor === p.id && 'opacity-100',
|
|
133
|
+
)}
|
|
134
|
+
aria-label={t('providers.row_menu', { name: p.name })}
|
|
135
|
+
title={t('providers.row_menu', { name: p.name })}
|
|
136
|
+
><EllipsisIcon class="size-4" /></button
|
|
137
|
+
>{/snippet}</DropdownMenu.Trigger
|
|
138
|
+
>
|
|
139
|
+
<DropdownMenu.Content align="start" class="min-w-36">
|
|
140
|
+
<DropdownMenu.Item onSelect={() => onaction(p, 'edit')}
|
|
141
|
+
><SquarePenIcon />{t('providers.edit')}</DropdownMenu.Item
|
|
142
|
+
>
|
|
143
|
+
<DropdownMenu.Item onSelect={() => onaction(p, 'notes')}
|
|
144
|
+
><UserPenIcon />{t('providers.notes')}</DropdownMenu.Item
|
|
145
|
+
>
|
|
146
|
+
{#if !p.presetId}<DropdownMenu.Item
|
|
147
|
+
variant="destructive"
|
|
148
|
+
onSelect={() => onaction(p, 'delete')}
|
|
149
|
+
><Trash2Icon />{t('providers.remove')}</DropdownMenu.Item
|
|
150
|
+
>{/if}
|
|
151
|
+
</DropdownMenu.Content>
|
|
152
|
+
</DropdownMenu.Root>
|
|
153
|
+
</li>
|
|
154
|
+
{:else}<li class="text-muted-foreground px-2 py-4 text-center text-kit-sm">
|
|
155
|
+
{t('providers.no_matches')}
|
|
156
|
+
</li>{/each}
|
|
157
|
+
</ul>
|
|
158
|
+
<Button variant="outline" class="mt-2" onclick={onadd}><PlusIcon />{t('providers.add')}</Button>
|
|
159
|
+
</nav>
|