@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,29 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// Provider settings (docs/33): list on the left, detail on the right, every field saves itself.
|
|
3
3
|
import { Button } from '#lib/components/ui/button/index.js'
|
|
4
|
-
import * as Dialog from '#lib/components/ui/dialog/index.js'
|
|
5
|
-
import * as DropdownMenu from '#lib/components/ui/dropdown-menu/index.js'
|
|
6
4
|
import * as Empty from '#lib/components/ui/empty/index.js'
|
|
7
5
|
import * as Field from '#lib/components/ui/field/index.js'
|
|
8
|
-
import { Input } from '#lib/components/ui/input/index.js'
|
|
9
6
|
import * as Select from '#lib/components/ui/select/index.js'
|
|
10
7
|
import { Switch } from '#lib/components/ui/switch/index.js'
|
|
11
|
-
import { Textarea } from '#lib/components/ui/textarea/index.js'
|
|
12
|
-
import EllipsisIcon from '@lucide/svelte/icons/ellipsis-vertical'
|
|
13
|
-
import SquarePenIcon from '@lucide/svelte/icons/square-pen'
|
|
14
|
-
import Trash2Icon from '@lucide/svelte/icons/trash-2'
|
|
15
|
-
import UserPenIcon from '@lucide/svelte/icons/user-pen'
|
|
16
|
-
import PlusIcon from '@lucide/svelte/icons/plus'
|
|
17
|
-
import SearchIcon from '@lucide/svelte/icons/search'
|
|
18
8
|
import Settings2Icon from '@lucide/svelte/icons/settings-2'
|
|
19
|
-
import
|
|
9
|
+
import PlusIcon from '@lucide/svelte/icons/plus'
|
|
20
10
|
import { getKitContext } from '../context'
|
|
21
11
|
import { isKitClientError, type ProbeResult, type Provider, type ProviderPreset } from '../ports'
|
|
22
|
-
import { cn } from '../utils'
|
|
23
12
|
import { newEndpoint, savable, toDraft, validateModel, type ProviderDraft } from './draft'
|
|
24
13
|
import { createProviderEditSession, type ProviderEditSession } from './edit-session'
|
|
25
14
|
import EndpointPanel, { type CheckOutcome, type CheckState } from './EndpointPanel.svelte'
|
|
26
15
|
import ModelPickerDialog from './ModelPickerDialog.svelte'
|
|
16
|
+
import ProviderDialogs from './ProviderDialogs.svelte'
|
|
17
|
+
import ProviderList from './ProviderList.svelte'
|
|
27
18
|
import { providersRevision, settingsTarget } from './revision.svelte'
|
|
28
19
|
|
|
29
20
|
let {
|
|
@@ -39,7 +30,6 @@
|
|
|
39
30
|
const { ports, i18n, notify } = getKitContext()
|
|
40
31
|
const { t } = i18n
|
|
41
32
|
const port = ports.providers
|
|
42
|
-
const FILTERS = ['all', 'enabled', 'disabled'] as const
|
|
43
33
|
|
|
44
34
|
let providers = $state<Provider[]>([])
|
|
45
35
|
let draft = $state<ProviderDraft | null>(null)
|
|
@@ -53,26 +43,15 @@
|
|
|
53
43
|
/** Last rejected model setting, named in the banner (docs/41 §4.3). */
|
|
54
44
|
let invalid = $state<{ model: string | null; message: string } | null>(null)
|
|
55
45
|
let enabledSaving = $state(false)
|
|
56
|
-
let query = $state('')
|
|
57
|
-
let filter = $state<(typeof FILTERS)[number]>('all')
|
|
58
46
|
let addOpen = $state(false)
|
|
59
47
|
let newName = $state('')
|
|
60
48
|
let infoOpen = $state(false)
|
|
61
49
|
let editName = $state('')
|
|
62
50
|
let notesOpen = $state(false)
|
|
63
51
|
let editNotes = $state('')
|
|
64
|
-
/** Provider whose row menu is open (hover ⋯ or right-click). */
|
|
65
|
-
let menuFor = $state<string | null>(null)
|
|
66
52
|
let editSession: ProviderEditSession | null = null
|
|
67
53
|
let probeGeneration = 0
|
|
68
54
|
|
|
69
|
-
const visible = $derived(
|
|
70
|
-
providers.filter(
|
|
71
|
-
(p) =>
|
|
72
|
-
p.name.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()) &&
|
|
73
|
-
(filter === 'all' || (filter === 'enabled') === p.enabled),
|
|
74
|
-
),
|
|
75
|
-
)
|
|
76
55
|
const endpoint = $derived(draft?.endpoints[active] ?? null)
|
|
77
56
|
|
|
78
57
|
function errorText(error: unknown): string {
|
|
@@ -87,25 +66,26 @@
|
|
|
87
66
|
pricing: true,
|
|
88
67
|
}
|
|
89
68
|
/** A rejected model setting: the backend names field + reason; the model is `entity` or found by the local rules. */
|
|
90
|
-
function modelError(
|
|
69
|
+
function modelError(
|
|
70
|
+
error: unknown,
|
|
71
|
+
sent: ProviderDraft,
|
|
72
|
+
): { model: string | null; message: string } | null {
|
|
91
73
|
if (!isKitClientError(error) || error.code !== 'bad_request') return null
|
|
92
74
|
const { field, reason, entity } = error.details
|
|
93
75
|
if (!field || !reason || !Object.hasOwn(MODEL_FIELDS, field)) return null
|
|
94
|
-
const models = sent.endpoints.flatMap((e) =>
|
|
76
|
+
const models = sent.endpoints.flatMap((e) =>
|
|
77
|
+
e.models.map((model) => ({ model, protocol: e.protocol })),
|
|
78
|
+
)
|
|
95
79
|
const found =
|
|
96
80
|
models.find(({ model }) => model.id === entity) ??
|
|
97
81
|
models.find(({ model, protocol }) =>
|
|
98
|
-
validateModel(model, protocol).some(
|
|
82
|
+
validateModel(model, protocol).some(
|
|
83
|
+
(item) => item.field === field && item.reason === reason,
|
|
84
|
+
),
|
|
99
85
|
)
|
|
100
86
|
const model = found ? (found.model.name ?? found.model.id) : null
|
|
101
87
|
return { model, message: t(`providers.error.${field}.${reason}`) }
|
|
102
88
|
}
|
|
103
|
-
function status(p: Provider): string | null {
|
|
104
|
-
if (!p.enabled) return null
|
|
105
|
-
return p.endpoints.length === 0 || p.endpoints.some((e) => !e.keys.some((k) => k.enabled && k.readable))
|
|
106
|
-
? 'bg-warning'
|
|
107
|
-
: 'bg-success'
|
|
108
|
-
}
|
|
109
89
|
function notifyChanged() {
|
|
110
90
|
providersRevision.value++
|
|
111
91
|
onchanged?.()
|
|
@@ -151,7 +131,13 @@
|
|
|
151
131
|
}
|
|
152
132
|
async function select(p: Provider) {
|
|
153
133
|
if (p.id === draft?.id) return
|
|
154
|
-
if (
|
|
134
|
+
if (
|
|
135
|
+
failed &&
|
|
136
|
+
!(await notify.confirm({
|
|
137
|
+
title: t('providers.discard_title'),
|
|
138
|
+
body: t('providers.discard_body'),
|
|
139
|
+
}))
|
|
140
|
+
)
|
|
155
141
|
return
|
|
156
142
|
load(toDraft(p))
|
|
157
143
|
}
|
|
@@ -166,11 +152,14 @@
|
|
|
166
152
|
name: preset.name,
|
|
167
153
|
presetId: preset.id,
|
|
168
154
|
enabled: false,
|
|
169
|
-
endpoints: [
|
|
155
|
+
endpoints: [
|
|
156
|
+
{ slug: 'ep-1', baseUrl: preset.baseUrl, protocol: preset.protocol, models: [] },
|
|
157
|
+
],
|
|
170
158
|
})
|
|
171
159
|
}
|
|
172
160
|
const target = settingsTarget.value
|
|
173
|
-
const owner =
|
|
161
|
+
const owner =
|
|
162
|
+
target && providers.find((p) => p.endpoints.some((e) => e.id === target.endpointId))
|
|
174
163
|
if (!owner) settingsTarget.value = null
|
|
175
164
|
load(owner ? toDraft(owner) : providers[0] ? toDraft(providers[0]) : null)
|
|
176
165
|
// EndpointPanel opens the target model's dialog when it mounts (and clears the target).
|
|
@@ -184,7 +173,8 @@
|
|
|
184
173
|
}
|
|
185
174
|
}
|
|
186
175
|
const keyUrl = $derived(
|
|
187
|
-
presets.find((preset) => preset.id === providers.find((p) => p.id === draft?.id)?.presetId)
|
|
176
|
+
presets.find((preset) => preset.id === providers.find((p) => p.id === draft?.id)?.presetId)
|
|
177
|
+
?.apiKeyUrl,
|
|
188
178
|
)
|
|
189
179
|
$effect(() => {
|
|
190
180
|
if (port) void refresh()
|
|
@@ -246,7 +236,6 @@
|
|
|
246
236
|
}
|
|
247
237
|
/** Row menu actions act on that row, so select it first (honours the unsaved-changes prompt). */
|
|
248
238
|
async function rowAction(p: Provider, action: 'edit' | 'notes' | 'delete') {
|
|
249
|
-
menuFor = null
|
|
250
239
|
await select(p)
|
|
251
240
|
if (draft?.id !== p.id) return
|
|
252
241
|
if (action === 'delete') return remove()
|
|
@@ -359,101 +348,20 @@
|
|
|
359
348
|
: { slug, value: { state: 'error', message: errorText(error) } }
|
|
360
349
|
}
|
|
361
350
|
}
|
|
362
|
-
const probed = $derived(
|
|
351
|
+
const probed = $derived(
|
|
352
|
+
probe ? (draft?.endpoints.find((e) => e.slug === probe?.slug) ?? null) : null,
|
|
353
|
+
)
|
|
363
354
|
</script>
|
|
364
355
|
|
|
365
356
|
{#if port}
|
|
366
357
|
<div class="flex min-h-0 min-w-0 flex-col gap-4 sm:flex-row">
|
|
367
|
-
<
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
class="pl-8"
|
|
375
|
-
placeholder={t('providers.search')}
|
|
376
|
-
aria-label={t('providers.search')}
|
|
377
|
-
bind:value={query}
|
|
378
|
-
autocomplete="off"
|
|
379
|
-
/>
|
|
380
|
-
</div>
|
|
381
|
-
<Select.Root type="single" value={filter} onValueChange={(value) => (filter = value as typeof filter)}>
|
|
382
|
-
<Select.Trigger
|
|
383
|
-
class={cn('shrink-0 px-2 [&>svg:last-child]:hidden', filter !== 'all' && 'bg-accent')}
|
|
384
|
-
aria-label={t('providers.filter')}
|
|
385
|
-
title={t(`providers.filter_${filter}`)}
|
|
386
|
-
>
|
|
387
|
-
<SlidersHorizontalIcon />
|
|
388
|
-
</Select.Trigger>
|
|
389
|
-
<Select.Content>
|
|
390
|
-
<Select.Group>
|
|
391
|
-
{#each FILTERS as f (f)}<Select.Item value={f} label={t(`providers.filter_${f}`)} />{/each}
|
|
392
|
-
</Select.Group>
|
|
393
|
-
</Select.Content>
|
|
394
|
-
</Select.Root>
|
|
395
|
-
</div>
|
|
396
|
-
<ul class="flex min-w-0 flex-col gap-0.5">
|
|
397
|
-
{#each visible as p (p.id)}
|
|
398
|
-
{@const dot = status(p)}
|
|
399
|
-
<li
|
|
400
|
-
class="group/row relative"
|
|
401
|
-
oncontextmenu={(event) => {
|
|
402
|
-
event.preventDefault()
|
|
403
|
-
menuFor = p.id
|
|
404
|
-
}}
|
|
405
|
-
>
|
|
406
|
-
<button
|
|
407
|
-
type="button"
|
|
408
|
-
class={cn(
|
|
409
|
-
'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',
|
|
410
|
-
draft?.id === p.id && 'bg-muted font-medium',
|
|
411
|
-
)}
|
|
412
|
-
aria-current={draft?.id === p.id ? 'true' : undefined}
|
|
413
|
-
onclick={() => void select(p)}
|
|
414
|
-
>
|
|
415
|
-
<span
|
|
416
|
-
aria-hidden="true"
|
|
417
|
-
class="bg-primary/10 text-primary flex size-6 shrink-0 items-center justify-center rounded-md text-kit-xs font-semibold"
|
|
418
|
-
>{p.name.slice(0, 1).toUpperCase()}</span
|
|
419
|
-
>
|
|
420
|
-
<span class={cn('min-w-0 flex-1 truncate', !p.enabled && 'text-muted-foreground')}>{p.name}</span>
|
|
421
|
-
{#if dot}<span class={cn('size-1.5 shrink-0 rounded-full', dot)}></span><span class="sr-only"
|
|
422
|
-
>{t(dot === 'bg-success' ? 'providers.filter_enabled' : 'providers.needs_setup')}</span
|
|
423
|
-
>{/if}
|
|
424
|
-
</button>
|
|
425
|
-
<DropdownMenu.Root open={menuFor === p.id} onOpenChange={(open) => (menuFor = open ? p.id : null)}>
|
|
426
|
-
<DropdownMenu.Trigger>
|
|
427
|
-
{#snippet child({ props })}<button
|
|
428
|
-
{...props}
|
|
429
|
-
type="button"
|
|
430
|
-
class={cn(
|
|
431
|
-
'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',
|
|
432
|
-
menuFor === p.id && 'opacity-100',
|
|
433
|
-
)}
|
|
434
|
-
aria-label={t('providers.row_menu', { name: p.name })}
|
|
435
|
-
title={t('providers.row_menu', { name: p.name })}><EllipsisIcon class="size-4" /></button
|
|
436
|
-
>{/snippet}
|
|
437
|
-
</DropdownMenu.Trigger>
|
|
438
|
-
<DropdownMenu.Content align="start" class="min-w-36">
|
|
439
|
-
<DropdownMenu.Item onSelect={() => void rowAction(p, 'edit')}
|
|
440
|
-
><SquarePenIcon />{t('providers.edit')}</DropdownMenu.Item
|
|
441
|
-
>
|
|
442
|
-
<DropdownMenu.Item onSelect={() => void rowAction(p, 'notes')}
|
|
443
|
-
><UserPenIcon />{t('providers.notes')}</DropdownMenu.Item
|
|
444
|
-
>
|
|
445
|
-
{#if !p.presetId}<DropdownMenu.Item variant="destructive" onSelect={() => void rowAction(p, 'delete')}
|
|
446
|
-
><Trash2Icon />{t('providers.remove')}</DropdownMenu.Item
|
|
447
|
-
>{/if}
|
|
448
|
-
</DropdownMenu.Content>
|
|
449
|
-
</DropdownMenu.Root>
|
|
450
|
-
</li>
|
|
451
|
-
{:else}
|
|
452
|
-
<li class="text-muted-foreground px-2 py-4 text-center text-kit-sm">{t('providers.no_matches')}</li>
|
|
453
|
-
{/each}
|
|
454
|
-
</ul>
|
|
455
|
-
<Button variant="outline" class="mt-2" onclick={() => (addOpen = true)}><PlusIcon />{t('providers.add')}</Button>
|
|
456
|
-
</nav>
|
|
358
|
+
<ProviderList
|
|
359
|
+
{providers}
|
|
360
|
+
selectedId={draft?.id ?? null}
|
|
361
|
+
onselect={(provider) => void select(provider)}
|
|
362
|
+
onaction={(provider, action) => void rowAction(provider, action)}
|
|
363
|
+
onadd={() => (addOpen = true)}
|
|
364
|
+
/>
|
|
457
365
|
|
|
458
366
|
<section class="flex min-w-0 flex-1 flex-col gap-5">
|
|
459
367
|
{#if draft}
|
|
@@ -483,7 +391,10 @@
|
|
|
483
391
|
<p role="alert" class="text-destructive flex items-center gap-2 text-kit-sm">
|
|
484
392
|
{invalid
|
|
485
393
|
? invalid.model
|
|
486
|
-
? t('providers.save_model_invalid', {
|
|
394
|
+
? t('providers.save_model_invalid', {
|
|
395
|
+
model: invalid.model,
|
|
396
|
+
error: invalid.message,
|
|
397
|
+
})
|
|
487
398
|
: invalid.message
|
|
488
399
|
: t('providers.save_failed')}
|
|
489
400
|
<Button variant="link" size="sm" class="h-auto p-0" onclick={() => void commit()}
|
|
@@ -493,15 +404,24 @@
|
|
|
493
404
|
{/if}
|
|
494
405
|
{#if draft.endpoints.length > 1}
|
|
495
406
|
<Field.Field orientation="horizontal" class="gap-2">
|
|
496
|
-
<Field.Label for="kit-provider-endpoint" class="shrink-0"
|
|
497
|
-
|
|
407
|
+
<Field.Label for="kit-provider-endpoint" class="shrink-0"
|
|
408
|
+
>{t('providers.endpoints')}</Field.Label
|
|
409
|
+
>
|
|
410
|
+
<Select.Root
|
|
411
|
+
type="single"
|
|
412
|
+
value={String(active)}
|
|
413
|
+
onValueChange={(value) => (active = Number(value))}
|
|
414
|
+
>
|
|
498
415
|
<Select.Trigger id="kit-provider-endpoint" class="min-w-0 flex-1">
|
|
499
416
|
<span class="truncate">{endpoint?.baseUrl || t('providers.endpoint_new')}</span>
|
|
500
417
|
</Select.Trigger>
|
|
501
418
|
<Select.Content>
|
|
502
419
|
<Select.Group>
|
|
503
420
|
{#each draft.endpoints as e, index (e.slug)}
|
|
504
|
-
<Select.Item
|
|
421
|
+
<Select.Item
|
|
422
|
+
value={String(index)}
|
|
423
|
+
label={e.baseUrl || t('providers.endpoint_new')}
|
|
424
|
+
/>
|
|
505
425
|
{/each}
|
|
506
426
|
</Select.Group>
|
|
507
427
|
</Select.Content>
|
|
@@ -554,78 +474,18 @@
|
|
|
554
474
|
void commit()
|
|
555
475
|
}}
|
|
556
476
|
/>
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
</Field.Field>
|
|
572
|
-
<Dialog.Footer>
|
|
573
|
-
<Button type="button" variant="outline" onclick={() => (addOpen = false)}>{t('common.cancel')}</Button>
|
|
574
|
-
<Button type="submit" disabled={!newName.trim()}>{t('common.confirm')}</Button>
|
|
575
|
-
</Dialog.Footer>
|
|
576
|
-
</form>
|
|
577
|
-
</Dialog.Content>
|
|
578
|
-
</Dialog.Root>
|
|
579
|
-
|
|
580
|
-
<Dialog.Root bind:open={infoOpen}>
|
|
581
|
-
<Dialog.Content class="sm:max-w-md">
|
|
582
|
-
<Dialog.Header><Dialog.Title>{t('providers.settings')}</Dialog.Title></Dialog.Header>
|
|
583
|
-
<form
|
|
584
|
-
class="contents"
|
|
585
|
-
onsubmit={(event) => {
|
|
586
|
-
event.preventDefault()
|
|
587
|
-
rename()
|
|
588
|
-
}}
|
|
589
|
-
>
|
|
590
|
-
<Field.Field>
|
|
591
|
-
<Field.Label for="kit-provider-name">{t('providers.name')}</Field.Label>
|
|
592
|
-
<Input id="kit-provider-name" maxlength={80} bind:value={editName} />
|
|
593
|
-
</Field.Field>
|
|
594
|
-
<Dialog.Footer class="sm:justify-between">
|
|
595
|
-
{#if !providers.find((p) => p.id === draft?.id)?.presetId}<Button
|
|
596
|
-
type="button"
|
|
597
|
-
variant="ghost"
|
|
598
|
-
class="text-destructive"
|
|
599
|
-
onclick={() => void remove()}>{t('providers.delete')}</Button
|
|
600
|
-
>{:else}<span></span>{/if}
|
|
601
|
-
<Button type="submit" disabled={!editName.trim()}>{t('providers.save')}</Button>
|
|
602
|
-
</Dialog.Footer>
|
|
603
|
-
</form>
|
|
604
|
-
</Dialog.Content>
|
|
605
|
-
</Dialog.Root>
|
|
606
|
-
|
|
607
|
-
<Dialog.Root bind:open={notesOpen}>
|
|
608
|
-
<Dialog.Content class="sm:max-w-lg">
|
|
609
|
-
<Dialog.Header><Dialog.Title>{t('providers.notes')}</Dialog.Title></Dialog.Header>
|
|
610
|
-
<form
|
|
611
|
-
class="contents"
|
|
612
|
-
onsubmit={(event) => {
|
|
613
|
-
event.preventDefault()
|
|
614
|
-
saveNotes()
|
|
615
|
-
}}
|
|
616
|
-
>
|
|
617
|
-
<Textarea
|
|
618
|
-
class="min-h-64 font-mono text-kit-sm"
|
|
619
|
-
aria-label={t('providers.notes')}
|
|
620
|
-
placeholder={t('providers.notes_placeholder')}
|
|
621
|
-
maxlength={10000}
|
|
622
|
-
bind:value={editNotes}
|
|
623
|
-
/>
|
|
624
|
-
<Dialog.Footer>
|
|
625
|
-
<Button type="button" variant="outline" onclick={() => (notesOpen = false)}>{t('common.cancel')}</Button>
|
|
626
|
-
<Button type="submit">{t('providers.save')}</Button>
|
|
627
|
-
</Dialog.Footer>
|
|
628
|
-
</form>
|
|
629
|
-
</Dialog.Content>
|
|
630
|
-
</Dialog.Root>
|
|
477
|
+
<ProviderDialogs
|
|
478
|
+
bind:addOpen
|
|
479
|
+
bind:infoOpen
|
|
480
|
+
bind:notesOpen
|
|
481
|
+
bind:newName
|
|
482
|
+
bind:editName
|
|
483
|
+
bind:editNotes
|
|
484
|
+
{draft}
|
|
485
|
+
{providers}
|
|
486
|
+
{create}
|
|
487
|
+
{rename}
|
|
488
|
+
{saveNotes}
|
|
489
|
+
{remove}
|
|
490
|
+
/>
|
|
631
491
|
{/if}
|
|
@@ -84,7 +84,8 @@
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
/** Same rule as the backend `image_base`: optional, http(s), ≤2048 bytes. */
|
|
87
|
-
const badImage = (url: string) =>
|
|
87
|
+
const badImage = (url: string) =>
|
|
88
|
+
!!url && (!validUrl(url) || new TextEncoder().encode(url).length > 2048)
|
|
88
89
|
function save() {
|
|
89
90
|
error = null
|
|
90
91
|
const url = baseUrl.trim()
|
|
@@ -125,13 +126,20 @@
|
|
|
125
126
|
<Field.Group>
|
|
126
127
|
<Field.Field>
|
|
127
128
|
<Field.Label for="kit-endpoint-protocol">{t('providers.protocol')}</Field.Label>
|
|
128
|
-
<Select.Root
|
|
129
|
+
<Select.Root
|
|
130
|
+
type="single"
|
|
131
|
+
value={protocol}
|
|
132
|
+
onValueChange={(value) => (protocol = value as Protocol)}
|
|
133
|
+
>
|
|
129
134
|
<Select.Trigger id="kit-endpoint-protocol" class="w-full"
|
|
130
135
|
>{t(`providers.protocol.${protocol}`)}</Select.Trigger
|
|
131
136
|
>
|
|
132
137
|
<Select.Content>
|
|
133
138
|
<Select.Group>
|
|
134
|
-
{#each PROTOCOLS as p (p)}<Select.Item
|
|
139
|
+
{#each PROTOCOLS as p (p)}<Select.Item
|
|
140
|
+
value={p}
|
|
141
|
+
label={t(`providers.protocol.${p}`)}
|
|
142
|
+
/>{/each}
|
|
135
143
|
</Select.Group>
|
|
136
144
|
</Select.Content>
|
|
137
145
|
</Select.Root>
|
|
@@ -139,7 +147,9 @@
|
|
|
139
147
|
<Field.Field>
|
|
140
148
|
<div class="flex items-center gap-2">
|
|
141
149
|
<Field.Label for="kit-request-url">{t('providers.base_url')}</Field.Label>
|
|
142
|
-
<span class="bg-primary/10 text-primary rounded-kit px-1.5 text-kit-xs"
|
|
150
|
+
<span class="bg-primary/10 text-primary rounded-kit px-1.5 text-kit-xs"
|
|
151
|
+
>{t('providers.default')}</span
|
|
152
|
+
>
|
|
143
153
|
</div>
|
|
144
154
|
<CopyInput
|
|
145
155
|
id="kit-request-url"
|
|
@@ -149,7 +159,9 @@
|
|
|
149
159
|
/>
|
|
150
160
|
{#if validUrl(baseUrl)}
|
|
151
161
|
<Field.Description class="truncate font-mono text-kit-xs"
|
|
152
|
-
>{t('providers.base_url_hint', {
|
|
162
|
+
>{t('providers.base_url_hint', {
|
|
163
|
+
url: `POST ${chatUrl(baseUrl, protocol)}`,
|
|
164
|
+
})}</Field.Description
|
|
153
165
|
>
|
|
154
166
|
{/if}
|
|
155
167
|
</Field.Field>
|
|
@@ -171,26 +183,36 @@
|
|
|
171
183
|
placeholder="https://example.com"
|
|
172
184
|
bind:value={imageBaseUrl}
|
|
173
185
|
/>
|
|
174
|
-
<Field.Description class="text-kit-xs"
|
|
186
|
+
<Field.Description class="text-kit-xs"
|
|
187
|
+
>{t('providers.image_base_url_hint')}</Field.Description
|
|
188
|
+
>
|
|
175
189
|
</Field.Field>
|
|
176
190
|
<Field.Field>
|
|
177
|
-
<Field.Label for="kit-request-image-edit-url"
|
|
191
|
+
<Field.Label for="kit-request-image-edit-url"
|
|
192
|
+
>{t('providers.image_edit_base_url')}</Field.Label
|
|
193
|
+
>
|
|
178
194
|
<CopyInput
|
|
179
195
|
id="kit-request-image-edit-url"
|
|
180
196
|
class="font-mono"
|
|
181
197
|
placeholder="https://example.com"
|
|
182
198
|
bind:value={imageEditBaseUrl}
|
|
183
199
|
/>
|
|
184
|
-
<Field.Description class="text-kit-xs"
|
|
200
|
+
<Field.Description class="text-kit-xs"
|
|
201
|
+
>{t('providers.image_edit_base_url_hint')}</Field.Description
|
|
202
|
+
>
|
|
185
203
|
</Field.Field>
|
|
186
204
|
<Field.Field>
|
|
187
205
|
<div class="flex items-center justify-between gap-2">
|
|
188
|
-
<Field.Label for={json === null ? undefined : 'kit-request-headers'}
|
|
206
|
+
<Field.Label for={json === null ? undefined : 'kit-request-headers'}
|
|
207
|
+
>{t('providers.headers')}</Field.Label
|
|
208
|
+
>
|
|
189
209
|
<Button
|
|
190
210
|
variant="ghost"
|
|
191
211
|
size="icon"
|
|
192
212
|
title={json === null ? t('providers.headers_to_json') : t('providers.headers_to_list')}
|
|
193
|
-
aria-label={json === null
|
|
213
|
+
aria-label={json === null
|
|
214
|
+
? t('providers.headers_to_json')
|
|
215
|
+
: t('providers.headers_to_list')}
|
|
194
216
|
onclick={toggle}
|
|
195
217
|
>{#if json === null}<BracesIcon />{:else}<ListIcon />{/if}</Button
|
|
196
218
|
>
|
|
@@ -222,10 +244,16 @@
|
|
|
222
244
|
variant="ghost"
|
|
223
245
|
class="self-start"
|
|
224
246
|
disabled={rows.length >= 20}
|
|
225
|
-
onclick={() => rows.push({ name: '', value: '' })}
|
|
247
|
+
onclick={() => rows.push({ name: '', value: '' })}
|
|
248
|
+
><PlusIcon />{t('providers.headers_add')}</Button
|
|
226
249
|
>
|
|
227
250
|
{:else}
|
|
228
|
-
<Textarea
|
|
251
|
+
<Textarea
|
|
252
|
+
id="kit-request-headers"
|
|
253
|
+
class="min-h-32 font-mono text-xs"
|
|
254
|
+
spellcheck={false}
|
|
255
|
+
bind:value={json}
|
|
256
|
+
/>
|
|
229
257
|
{/if}
|
|
230
258
|
</Field.Field>
|
|
231
259
|
</Field.Group>
|
|
@@ -83,7 +83,10 @@
|
|
|
83
83
|
if (copying) await copy()
|
|
84
84
|
} catch (error) {
|
|
85
85
|
if (session.isCurrent(token))
|
|
86
|
-
notify.toast(
|
|
86
|
+
notify.toast(
|
|
87
|
+
'error',
|
|
88
|
+
error instanceof Error ? error.message : t('providers.key_reveal_failed'),
|
|
89
|
+
)
|
|
87
90
|
} finally {
|
|
88
91
|
if (session.isCurrent(token)) {
|
|
89
92
|
busy = false
|
|
@@ -143,7 +146,10 @@
|
|
|
143
146
|
if (!endpointId || !keyId) return
|
|
144
147
|
const token = session.capture()
|
|
145
148
|
if (
|
|
146
|
-
!(await notify.confirm({
|
|
149
|
+
!(await notify.confirm({
|
|
150
|
+
title: t('providers.key_delete'),
|
|
151
|
+
body: t('providers.key_delete_confirm'),
|
|
152
|
+
})) ||
|
|
147
153
|
!session.isCurrent(token)
|
|
148
154
|
)
|
|
149
155
|
return
|
|
@@ -189,7 +195,9 @@
|
|
|
189
195
|
class="flex h-full min-w-0 flex-1 items-center gap-1.5 pl-2.5 text-left outline-none"
|
|
190
196
|
onclick={() => manage(null)}
|
|
191
197
|
>
|
|
192
|
-
{#if primary.label}<span class="shrink-0 font-sans text-muted-foreground"
|
|
198
|
+
{#if primary.label}<span class="shrink-0 font-sans text-muted-foreground"
|
|
199
|
+
>{primary.label}</span
|
|
200
|
+
>{/if}
|
|
193
201
|
<span class="min-w-0 truncate">{plain ?? maskedText(primary, t)}</span>
|
|
194
202
|
{#if keys.length > 1}<span class="shrink-0 font-sans text-kit-xs text-muted-foreground"
|
|
195
203
|
>+{keys.length - 1}</span
|