@ouraihub/aether-ui 0.0.0-stage → 0.1.1
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/LICENSE +789 -0
- package/fixtures/model-classify.json +42 -0
- package/fixtures/pricing-cost.json +56 -0
- package/fixtures/reasoning-infer.json +44 -0
- package/fixtures/sse/anthropic-messages/chinese.sse +24 -0
- package/fixtures/sse/anthropic-messages/multi-tool.sse +33 -0
- package/fixtures/sse/anthropic-messages/single-tool.sse +39 -0
- package/fixtures/sse/anthropic-messages/text.sse +27 -0
- package/fixtures/sse/anthropic-messages/truncated.sse +21 -0
- package/fixtures/sse/anthropic-messages/upstream-error.sse +12 -0
- package/fixtures/sse/expect.json +20 -0
- package/fixtures/sse/openai-chat/chinese.sse +14 -0
- package/fixtures/sse/openai-chat/multi-tool.sse +18 -0
- package/fixtures/sse/openai-chat/single-tool.sse +22 -0
- package/fixtures/sse/openai-chat/text.sse +14 -0
- package/fixtures/sse/openai-chat/truncated.sse +12 -0
- package/fixtures/sse/openai-chat/upstream-error.sse +6 -0
- package/fixtures/sse/openai-responses/chinese.sse +33 -0
- package/fixtures/sse/openai-responses/multi-tool.sse +36 -0
- package/fixtures/sse/openai-responses/single-tool.sse +27 -0
- package/fixtures/sse/openai-responses/text.sse +33 -0
- package/fixtures/sse/openai-responses/truncated.sse +30 -0
- package/fixtures/sse/openai-responses/upstream-error.sse +9 -0
- package/package.json +71 -3
- package/src/CopyInput.svelte +33 -0
- package/src/KitNotifier.svelte +33 -0
- package/src/chat/ApprovalCard.svelte +29 -0
- package/src/chat/ChatComposer.svelte +277 -0
- package/src/chat/ChatMessage.svelte +262 -0
- package/src/chat/ChatPanel.css +386 -0
- package/src/chat/ChatPanel.svelte +496 -0
- package/src/chat/ChatPanelApprovals.svelte +28 -0
- package/src/chat/ChatPanelComposer.svelte +317 -0
- package/src/chat/ChatPanelDialogs.svelte +242 -0
- package/src/chat/ChatPanelHeader.svelte +132 -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 +425 -0
- package/src/chat/ChatSurface.svelte +168 -0
- package/src/chat/MessageItem.svelte +480 -0
- package/src/chat/ReasoningControl.svelte +286 -0
- package/src/chat/SkillPicker.svelte +133 -0
- package/src/chat/ToolCallItem.svelte +23 -0
- package/src/chat/UsageControl.svelte +216 -0
- package/src/chat/UsageDetails.svelte +76 -0
- package/src/chat/chat-attachments.svelte.ts +74 -0
- package/src/chat/chat-panel-history.svelte.ts +430 -0
- package/src/chat/chat-panel-props.ts +32 -0
- package/src/chat/index.ts +6 -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 +494 -0
- package/src/chat/test-support/chat-fixture.ts +178 -0
- package/src/chat/test-support/conversation-fixtures.ts +32 -0
- package/src/chat/test-support/run-session-fixture.ts +112 -0
- package/src/components/ui/alert/alert-action.svelte +20 -0
- package/src/components/ui/alert/alert-description.svelte +23 -0
- package/src/components/ui/alert/alert-title.svelte +23 -0
- package/src/components/ui/alert/alert.svelte +43 -0
- package/src/components/ui/alert/index.ts +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
- package/src/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
- package/src/components/ui/alert-dialog/alert-dialog-content.svelte +32 -0
- package/src/components/ui/alert-dialog/alert-dialog-description.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
- package/src/components/ui/alert-dialog/alert-dialog-header.svelte +20 -0
- package/src/components/ui/alert-dialog/alert-dialog-media.svelte +20 -0
- package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
- package/src/components/ui/alert-dialog/alert-dialog-title.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
- package/src/components/ui/alert-dialog/alert-dialog.svelte +7 -0
- package/src/components/ui/alert-dialog/index.ts +40 -0
- package/src/components/ui/button/button.svelte +81 -0
- package/src/components/ui/button/index.ts +17 -0
- package/src/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/components/ui/dialog/dialog-content.svelte +48 -0
- package/src/components/ui/dialog/dialog-description.svelte +17 -0
- package/src/components/ui/dialog/dialog-footer.svelte +32 -0
- package/src/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/src/components/ui/dialog/dialog-portal.svelte +7 -0
- package/src/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/components/ui/dialog/dialog.svelte +7 -0
- package/src/components/ui/dialog/index.ts +34 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +46 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +36 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +31 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/src/components/ui/dropdown-menu/index.ts +54 -0
- package/src/components/ui/empty/empty-content.svelte +23 -0
- package/src/components/ui/empty/empty-description.svelte +23 -0
- package/src/components/ui/empty/empty-header.svelte +20 -0
- package/src/components/ui/empty/empty-media.svelte +41 -0
- package/src/components/ui/empty/empty-title.svelte +20 -0
- package/src/components/ui/empty/empty.svelte +23 -0
- package/src/components/ui/empty/index.ts +22 -0
- package/src/components/ui/field/field-content.svelte +20 -0
- package/src/components/ui/field/field-description.svelte +25 -0
- package/src/components/ui/field/field-error.svelte +58 -0
- package/src/components/ui/field/field-group.svelte +23 -0
- package/src/components/ui/field/field-label.svelte +25 -0
- package/src/components/ui/field/field-legend.svelte +24 -0
- package/src/components/ui/field/field-separator.svelte +35 -0
- package/src/components/ui/field/field-set.svelte +20 -0
- package/src/components/ui/field/field-title.svelte +20 -0
- package/src/components/ui/field/field.svelte +47 -0
- package/src/components/ui/field/index.ts +33 -0
- package/src/components/ui/input/index.ts +7 -0
- package/src/components/ui/input/input.svelte +48 -0
- package/src/components/ui/label/index.ts +7 -0
- package/src/components/ui/label/label.svelte +20 -0
- package/src/components/ui/select/index.ts +40 -0
- package/src/components/ui/select/select-content.svelte +45 -0
- package/src/components/ui/select/select-group-heading.svelte +21 -0
- package/src/components/ui/select/select-group.svelte +17 -0
- package/src/components/ui/select/select-item.svelte +43 -0
- package/src/components/ui/select/select-label.svelte +20 -0
- package/src/components/ui/select/select-portal.svelte +7 -0
- package/src/components/ui/select/select-scroll-down-button.svelte +20 -0
- package/src/components/ui/select/select-scroll-up-button.svelte +20 -0
- package/src/components/ui/select/select-separator.svelte +18 -0
- package/src/components/ui/select/select-trigger.svelte +29 -0
- package/src/components/ui/select/select-value.svelte +7 -0
- package/src/components/ui/select/select.svelte +11 -0
- package/src/components/ui/separator/index.ts +7 -0
- package/src/components/ui/separator/separator.svelte +21 -0
- package/src/components/ui/sonner/index.ts +1 -0
- package/src/components/ui/sonner/sonner.svelte +40 -0
- package/src/components/ui/switch/index.ts +7 -0
- package/src/components/ui/switch/switch.svelte +34 -0
- package/src/components/ui/textarea/index.ts +7 -0
- package/src/components/ui/textarea/textarea.svelte +22 -0
- package/src/context.ts +34 -0
- package/src/dev/Demo.svelte +47 -0
- package/src/dev/main.ts +7 -0
- package/src/i18n/en.ts +645 -0
- package/src/i18n/index.ts +28 -0
- package/src/i18n/zh-CN.ts +610 -0
- package/src/index.ts +10 -0
- package/src/logs/LogPanel.svelte +103 -0
- package/src/logs/LogSettings.svelte +58 -0
- package/src/logs/index.ts +5 -0
- package/src/logs/logger.ts +96 -0
- package/src/markdown.ts +8 -0
- package/src/mcp/McpClientEditor.svelte +204 -0
- package/src/mcp/McpClientSettings.svelte +418 -0
- package/src/mcp/McpServerSettings.svelte +363 -0
- package/src/mcp/config.ts +26 -0
- package/src/mcp/index.ts +3 -0
- package/src/mcp/input.ts +74 -0
- package/src/notify.svelte.ts +43 -0
- package/src/ports/contract.ts +174 -0
- package/src/ports/fake-optional.ts +81 -0
- package/src/ports/fake.ts +488 -0
- package/src/ports/index.ts +7 -0
- package/src/ports/search.ts +56 -0
- package/src/ports/skill-sources.ts +45 -0
- package/src/ports/test-support/fake-fixture.ts +63 -0
- package/src/providers/EndpointKeysDialog.svelte +357 -0
- package/src/providers/EndpointModels.svelte +311 -0
- package/src/providers/EndpointPanel.svelte +266 -0
- package/src/providers/ModelPickerDialog.svelte +230 -0
- package/src/providers/ModelPricing.svelte +125 -0
- package/src/providers/ModelSelect.svelte +448 -0
- package/src/providers/ModelSettingsDialog.svelte +388 -0
- package/src/providers/ProviderDialogs.svelte +120 -0
- package/src/providers/ProviderList.svelte +159 -0
- package/src/providers/ProvidersSettings.svelte +491 -0
- package/src/providers/RequestConfigDialog.svelte +278 -0
- package/src/providers/SecretInput.svelte +284 -0
- package/src/providers/draft.ts +355 -0
- package/src/providers/edit-session.ts +170 -0
- package/src/providers/index.ts +5 -0
- package/src/providers/reasoning.ts +55 -0
- package/src/providers/revision.svelte.ts +6 -0
- 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 +500 -0
- package/src/search/index.ts +13 -0
- package/src/search/settings-flow.ts +67 -0
- package/src/session.ts +20 -0
- package/src/skills/SkillAdd.svelte +380 -0
- package/src/skills/SkillAuthoring.svelte +430 -0
- package/src/skills/SkillEditor.svelte +104 -0
- package/src/skills/SkillPreviewDialog.svelte +136 -0
- package/src/skills/SkillsSettings.svelte +147 -0
- package/src/skills/error.ts +32 -0
- package/src/skills/index.ts +4 -0
- package/src/skills/test-support/skills-fixture.ts +139 -0
- package/src/styles/kit.css +137 -0
- package/src/styles/tokens.css +22 -0
- package/src/tauri/index.ts +298 -0
- package/src/utils.ts +22 -0
- package/src/web/index.ts +441 -0
- package/README.md +0 -3
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Provider settings (docs/33): list on the left, detail on the right, every field saves itself.
|
|
3
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
4
|
+
import * as Empty from '#lib/components/ui/empty/index.js'
|
|
5
|
+
import * as Field from '#lib/components/ui/field/index.js'
|
|
6
|
+
import * as Select from '#lib/components/ui/select/index.js'
|
|
7
|
+
import { Switch } from '#lib/components/ui/switch/index.js'
|
|
8
|
+
import Settings2Icon from '@lucide/svelte/icons/settings-2'
|
|
9
|
+
import PlusIcon from '@lucide/svelte/icons/plus'
|
|
10
|
+
import { getKitContext } from '../context'
|
|
11
|
+
import { isKitClientError, type ProbeResult, type Provider, type ProviderPreset } from '../ports'
|
|
12
|
+
import { newEndpoint, savable, toDraft, validateModel, type ProviderDraft } from './draft'
|
|
13
|
+
import { createProviderEditSession, type ProviderEditSession } from './edit-session'
|
|
14
|
+
import EndpointPanel, { type CheckOutcome, type CheckState } from './EndpointPanel.svelte'
|
|
15
|
+
import ModelPickerDialog from './ModelPickerDialog.svelte'
|
|
16
|
+
import ProviderDialogs from './ProviderDialogs.svelte'
|
|
17
|
+
import ProviderList from './ProviderList.svelte'
|
|
18
|
+
import { providersRevision, settingsTarget } from './revision.svelte'
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
onchanged,
|
|
22
|
+
presets = [],
|
|
23
|
+
usedBy,
|
|
24
|
+
}: {
|
|
25
|
+
onchanged?: () => void
|
|
26
|
+
presets?: ProviderPreset[]
|
|
27
|
+
/** Localized role names bound to a model, shown when its type changes (contract §6). */
|
|
28
|
+
usedBy?: (endpointId: string, modelId: string) => string[]
|
|
29
|
+
} = $props()
|
|
30
|
+
const { ports, i18n, notify } = getKitContext()
|
|
31
|
+
const { t } = i18n
|
|
32
|
+
const port = ports.providers
|
|
33
|
+
|
|
34
|
+
let providers = $state<Provider[]>([])
|
|
35
|
+
let draft = $state<ProviderDraft | null>(null)
|
|
36
|
+
/** Remounts the detail when another provider is loaded, resetting its local input state. */
|
|
37
|
+
let token = $state(0)
|
|
38
|
+
let active = $state(0)
|
|
39
|
+
let check = $state<{ slug: string; value: CheckState } | null>(null)
|
|
40
|
+
let probe = $state<{ slug: string; result: ProbeResult } | null>(null)
|
|
41
|
+
let pickerOpen = $state(false)
|
|
42
|
+
let failed = $state(false)
|
|
43
|
+
/** Last rejected model setting, named in the banner (docs/41 §4.3). */
|
|
44
|
+
let invalid = $state<{ model: string | null; message: string } | null>(null)
|
|
45
|
+
let enabledSaving = $state(false)
|
|
46
|
+
let addOpen = $state(false)
|
|
47
|
+
let newName = $state('')
|
|
48
|
+
let infoOpen = $state(false)
|
|
49
|
+
let editName = $state('')
|
|
50
|
+
let notesOpen = $state(false)
|
|
51
|
+
let editNotes = $state('')
|
|
52
|
+
let editSession: ProviderEditSession | null = null
|
|
53
|
+
let probeGeneration = 0
|
|
54
|
+
|
|
55
|
+
const endpoint = $derived(draft?.endpoints[active] ?? null)
|
|
56
|
+
|
|
57
|
+
function errorText(error: unknown): string {
|
|
58
|
+
return isKitClientError(error) ? t(`errors.${error.code}`) : String(error)
|
|
59
|
+
}
|
|
60
|
+
const MODEL_FIELDS: Record<string, true> = {
|
|
61
|
+
name: true,
|
|
62
|
+
groupName: true,
|
|
63
|
+
contextWindow: true,
|
|
64
|
+
maxOutput: true,
|
|
65
|
+
reasoning: true,
|
|
66
|
+
pricing: true,
|
|
67
|
+
}
|
|
68
|
+
/** A rejected model setting: the backend names field + reason; the model is `entity` or found by the local rules. */
|
|
69
|
+
function modelError(
|
|
70
|
+
error: unknown,
|
|
71
|
+
sent: ProviderDraft,
|
|
72
|
+
): { model: string | null; message: string } | null {
|
|
73
|
+
if (!isKitClientError(error) || error.code !== 'bad_request') return null
|
|
74
|
+
const { field, reason, entity } = error.details
|
|
75
|
+
if (!field || !reason || !Object.hasOwn(MODEL_FIELDS, field)) return null
|
|
76
|
+
const models = sent.endpoints.flatMap((e) =>
|
|
77
|
+
e.models.map((model) => ({ model, protocol: e.protocol })),
|
|
78
|
+
)
|
|
79
|
+
const found =
|
|
80
|
+
models.find(({ model }) => model.id === entity) ??
|
|
81
|
+
models.find(({ model, protocol }) =>
|
|
82
|
+
validateModel(model, protocol).some(
|
|
83
|
+
(item) => item.field === field && item.reason === reason,
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
const model = found ? (found.model.name ?? found.model.id) : null
|
|
87
|
+
return { model, message: t(`providers.error.${field}.${reason}`) }
|
|
88
|
+
}
|
|
89
|
+
function notifyChanged() {
|
|
90
|
+
providersRevision.value++
|
|
91
|
+
onchanged?.()
|
|
92
|
+
}
|
|
93
|
+
/** Key operations return the whole list; only the matching endpoint's stored record changes in the draft. */
|
|
94
|
+
function keysChanged(next: Provider[]) {
|
|
95
|
+
providers = next
|
|
96
|
+
const stored = next.find((p) => p.id === draft?.id)
|
|
97
|
+
for (const e of draft?.endpoints ?? []) {
|
|
98
|
+
const record = stored?.endpoints.find((s) => s.id === e.saved?.id)
|
|
99
|
+
if (record) e.saved = record
|
|
100
|
+
}
|
|
101
|
+
notifyChanged()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function load(next: ProviderDraft | null) {
|
|
105
|
+
editSession?.invalidate()
|
|
106
|
+
editSession = null
|
|
107
|
+
probeGeneration++
|
|
108
|
+
pickerOpen = false
|
|
109
|
+
check = null
|
|
110
|
+
probe = null
|
|
111
|
+
failed = false
|
|
112
|
+
invalid = null
|
|
113
|
+
draft = next
|
|
114
|
+
active = 0
|
|
115
|
+
token++
|
|
116
|
+
if (next && port)
|
|
117
|
+
editSession = createProviderEditSession(port, {
|
|
118
|
+
readDraft: () => {
|
|
119
|
+
if (!draft) throw new Error('Provider edit session has no draft')
|
|
120
|
+
return draft
|
|
121
|
+
},
|
|
122
|
+
writeDraft: (next) => {
|
|
123
|
+
draft = next
|
|
124
|
+
},
|
|
125
|
+
readProviders: () => providers,
|
|
126
|
+
onSaved: (next) => {
|
|
127
|
+
providers = next
|
|
128
|
+
notifyChanged()
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
async function select(p: Provider) {
|
|
133
|
+
if (p.id === draft?.id) return
|
|
134
|
+
if (
|
|
135
|
+
failed &&
|
|
136
|
+
!(await notify.confirm({
|
|
137
|
+
title: t('providers.discard_title'),
|
|
138
|
+
body: t('providers.discard_body'),
|
|
139
|
+
}))
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
load(toDraft(p))
|
|
143
|
+
}
|
|
144
|
+
async function refresh() {
|
|
145
|
+
if (!port) return
|
|
146
|
+
try {
|
|
147
|
+
providers = await port.list()
|
|
148
|
+
// Backend no-ops a preset already present and lets a same-named provider adopt it (docs/39 §3).
|
|
149
|
+
for (const preset of presets) {
|
|
150
|
+
if (providers.some((p) => p.presetId === preset.id)) continue
|
|
151
|
+
providers = await port.save({
|
|
152
|
+
name: preset.name,
|
|
153
|
+
presetId: preset.id,
|
|
154
|
+
enabled: false,
|
|
155
|
+
endpoints: [
|
|
156
|
+
{ slug: 'ep-1', baseUrl: preset.baseUrl, protocol: preset.protocol, models: [] },
|
|
157
|
+
],
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
const target = settingsTarget.value
|
|
161
|
+
const owner =
|
|
162
|
+
target && providers.find((p) => p.endpoints.some((e) => e.id === target.endpointId))
|
|
163
|
+
if (!owner) settingsTarget.value = null
|
|
164
|
+
load(owner ? toDraft(owner) : providers[0] ? toDraft(providers[0]) : null)
|
|
165
|
+
// EndpointPanel opens the target model's dialog when it mounts (and clears the target).
|
|
166
|
+
if (owner && target && draft)
|
|
167
|
+
active = Math.max(
|
|
168
|
+
0,
|
|
169
|
+
draft.endpoints.findIndex((e) => e.saved?.id === target.endpointId),
|
|
170
|
+
)
|
|
171
|
+
} catch (error) {
|
|
172
|
+
notify.toast('error', errorText(error))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const keyUrl = $derived(
|
|
176
|
+
presets.find((preset) => preset.id === providers.find((p) => p.id === draft?.id)?.presetId)
|
|
177
|
+
?.apiKeyUrl,
|
|
178
|
+
)
|
|
179
|
+
$effect(() => {
|
|
180
|
+
if (port) void refresh()
|
|
181
|
+
return () => {
|
|
182
|
+
editSession?.invalidate()
|
|
183
|
+
probeGeneration++
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
/** Presentation only; the ordinary TS session owns the queue and its save barrier. */
|
|
188
|
+
async function commit() {
|
|
189
|
+
const session = editSession
|
|
190
|
+
if (!session || !draft) return
|
|
191
|
+
const sent = $state.snapshot(savable(draft)) as ProviderDraft
|
|
192
|
+
try {
|
|
193
|
+
await session.commit()
|
|
194
|
+
if (editSession !== session) return
|
|
195
|
+
failed = false
|
|
196
|
+
invalid = null
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (editSession !== session || (isKitClientError(error) && error.code === 'cancelled')) return
|
|
199
|
+
failed = true
|
|
200
|
+
invalid = modelError(error, sent)
|
|
201
|
+
if (!invalid) notify.toast('error', errorText(error))
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function setEnabled(enabled: boolean) {
|
|
206
|
+
const id = draft?.id
|
|
207
|
+
if (!draft || !id || !port || enabledSaving) return
|
|
208
|
+
const previous = draft.enabled
|
|
209
|
+
draft.enabled = enabled
|
|
210
|
+
enabledSaving = true
|
|
211
|
+
try {
|
|
212
|
+
providers = await port.setEnabled(id, enabled)
|
|
213
|
+
notifyChanged()
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (draft?.id === id) draft.enabled = previous
|
|
216
|
+
notify.toast('error', errorText(error))
|
|
217
|
+
} finally {
|
|
218
|
+
enabledSaving = false
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function create() {
|
|
223
|
+
const name = newName.trim()
|
|
224
|
+
if (!name) return
|
|
225
|
+
addOpen = false
|
|
226
|
+
newName = ''
|
|
227
|
+
load({ id: null, name, enabled: true, notes: '', endpoints: [newEndpoint([])] })
|
|
228
|
+
await commit()
|
|
229
|
+
}
|
|
230
|
+
function rename() {
|
|
231
|
+
const name = editName.trim()
|
|
232
|
+
infoOpen = false
|
|
233
|
+
if (!draft || !name || name === draft.name) return
|
|
234
|
+
draft.name = name
|
|
235
|
+
void commit()
|
|
236
|
+
}
|
|
237
|
+
/** Row menu actions act on that row, so select it first (honours the unsaved-changes prompt). */
|
|
238
|
+
async function rowAction(p: Provider, action: 'edit' | 'notes' | 'delete') {
|
|
239
|
+
await select(p)
|
|
240
|
+
if (draft?.id !== p.id) return
|
|
241
|
+
if (action === 'delete') return remove()
|
|
242
|
+
editName = draft.name
|
|
243
|
+
editNotes = draft.notes
|
|
244
|
+
if (action === 'edit') infoOpen = true
|
|
245
|
+
else notesOpen = true
|
|
246
|
+
}
|
|
247
|
+
function saveNotes() {
|
|
248
|
+
notesOpen = false
|
|
249
|
+
if (!draft || editNotes === draft.notes) return
|
|
250
|
+
draft.notes = editNotes
|
|
251
|
+
void commit()
|
|
252
|
+
}
|
|
253
|
+
async function remove() {
|
|
254
|
+
const current = draft
|
|
255
|
+
const session = editSession
|
|
256
|
+
if (!current || !port) return
|
|
257
|
+
infoOpen = false
|
|
258
|
+
if (current.id === null) return load(providers[0] ? toDraft(providers[0]) : null)
|
|
259
|
+
const body = t('providers.delete_confirm', { name: current.name })
|
|
260
|
+
if (!(await notify.confirm({ title: t('providers.delete'), body, danger: true }))) return
|
|
261
|
+
if (editSession !== session) return
|
|
262
|
+
session?.invalidate()
|
|
263
|
+
probeGeneration++
|
|
264
|
+
pickerOpen = false
|
|
265
|
+
try {
|
|
266
|
+
const next = await port.remove(current.id)
|
|
267
|
+
if (editSession !== session) return
|
|
268
|
+
providers = next
|
|
269
|
+
load(providers[0] ? toDraft(providers[0]) : null)
|
|
270
|
+
notifyChanged()
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (editSession !== session) return
|
|
273
|
+
load(current)
|
|
274
|
+
notify.toast('error', errorText(error))
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function addEndpoint() {
|
|
278
|
+
if (!draft) return
|
|
279
|
+
draft.endpoints.push(newEndpoint(draft.endpoints))
|
|
280
|
+
active = draft.endpoints.length - 1
|
|
281
|
+
// Stored right away so its keys can be added before it has an address (docs/34 §2).
|
|
282
|
+
void commit()
|
|
283
|
+
}
|
|
284
|
+
async function removeEndpoint() {
|
|
285
|
+
if (!draft || !endpoint) return
|
|
286
|
+
const index = active
|
|
287
|
+
if (
|
|
288
|
+
endpoint.saved &&
|
|
289
|
+
!(await notify.confirm({
|
|
290
|
+
title: t('providers.remove_endpoint'),
|
|
291
|
+
body: t('providers.remove_endpoint_confirm'),
|
|
292
|
+
danger: true,
|
|
293
|
+
}))
|
|
294
|
+
)
|
|
295
|
+
return
|
|
296
|
+
const wasSaved = draft.endpoints[index]?.saved !== null
|
|
297
|
+
draft.endpoints.splice(index, 1)
|
|
298
|
+
active = 0
|
|
299
|
+
if (wasSaved) void commit()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function outcome(result: ProbeResult): CheckOutcome {
|
|
303
|
+
const e = result.error
|
|
304
|
+
if (!e) return { state: 'ok', count: result.models.length, suggested: result.suggestedProtocol }
|
|
305
|
+
const message = [
|
|
306
|
+
t(`providers.probe.${e.kind}`, { status: e.status ?? '' }),
|
|
307
|
+
e.hint ? t(e.hint) : '',
|
|
308
|
+
e.message ?? '',
|
|
309
|
+
]
|
|
310
|
+
.filter(Boolean)
|
|
311
|
+
.join('\n')
|
|
312
|
+
return { state: 'error', message }
|
|
313
|
+
}
|
|
314
|
+
async function runProbe(index: number, openPicker: boolean) {
|
|
315
|
+
const target = draft?.endpoints[index]
|
|
316
|
+
const session = editSession
|
|
317
|
+
if (!target || !session) return
|
|
318
|
+
const generation = ++probeGeneration
|
|
319
|
+
const slug = target.slug
|
|
320
|
+
check = { slug, value: { state: 'running' } }
|
|
321
|
+
try {
|
|
322
|
+
const batch = await session.probe(slug)
|
|
323
|
+
if (session !== editSession || generation !== probeGeneration) return
|
|
324
|
+
failed = false
|
|
325
|
+
invalid = null
|
|
326
|
+
const first = batch.results[0]
|
|
327
|
+
check = {
|
|
328
|
+
slug,
|
|
329
|
+
value:
|
|
330
|
+
first?.keyId === null
|
|
331
|
+
? outcome(first.result)
|
|
332
|
+
: {
|
|
333
|
+
state: 'keys',
|
|
334
|
+
results: batch.results.flatMap(({ keyId, result }) =>
|
|
335
|
+
keyId === null ? [] : [{ keyId, value: outcome(result) }],
|
|
336
|
+
),
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
if (openPicker && batch.usable) {
|
|
340
|
+
probe = { slug, result: batch.usable }
|
|
341
|
+
pickerOpen = true
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (session !== editSession || generation !== probeGeneration) return
|
|
345
|
+
check =
|
|
346
|
+
isKitClientError(error) && error.code === 'cancelled'
|
|
347
|
+
? null
|
|
348
|
+
: { slug, value: { state: 'error', message: errorText(error) } }
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const probed = $derived(
|
|
352
|
+
probe ? (draft?.endpoints.find((e) => e.slug === probe?.slug) ?? null) : null,
|
|
353
|
+
)
|
|
354
|
+
</script>
|
|
355
|
+
|
|
356
|
+
{#if port}
|
|
357
|
+
<div class="flex min-h-0 min-w-0 flex-col gap-4 sm:flex-row">
|
|
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
|
+
/>
|
|
365
|
+
|
|
366
|
+
<section class="flex min-w-0 flex-1 flex-col gap-5">
|
|
367
|
+
{#if draft}
|
|
368
|
+
<header class="flex min-h-8 min-w-0 items-center gap-1.5">
|
|
369
|
+
<h2 class="min-w-0 truncate text-[15px] font-semibold">{draft.name}</h2>
|
|
370
|
+
<Button
|
|
371
|
+
variant="ghost"
|
|
372
|
+
size="icon-xs"
|
|
373
|
+
class="text-muted-foreground"
|
|
374
|
+
title={t('providers.settings')}
|
|
375
|
+
aria-label={t('providers.settings')}
|
|
376
|
+
onclick={() => {
|
|
377
|
+
editName = draft?.name ?? ''
|
|
378
|
+
infoOpen = true
|
|
379
|
+
}}><Settings2Icon /></Button
|
|
380
|
+
>
|
|
381
|
+
<Switch
|
|
382
|
+
id="kit-provider-enabled"
|
|
383
|
+
class="ml-auto"
|
|
384
|
+
aria-label={t('providers.enabled')}
|
|
385
|
+
checked={draft.enabled}
|
|
386
|
+
disabled={enabledSaving || draft.id === null}
|
|
387
|
+
onCheckedChange={(enabled) => void setEnabled(enabled)}
|
|
388
|
+
/>
|
|
389
|
+
</header>
|
|
390
|
+
{#if failed}
|
|
391
|
+
<p role="alert" class="text-destructive flex items-center gap-2 text-kit-sm">
|
|
392
|
+
{invalid
|
|
393
|
+
? invalid.model
|
|
394
|
+
? t('providers.save_model_invalid', {
|
|
395
|
+
model: invalid.model,
|
|
396
|
+
error: invalid.message,
|
|
397
|
+
})
|
|
398
|
+
: invalid.message
|
|
399
|
+
: t('providers.save_failed')}
|
|
400
|
+
<Button variant="link" size="sm" class="h-auto p-0" onclick={() => void commit()}
|
|
401
|
+
>{t('providers.retry')}</Button
|
|
402
|
+
>
|
|
403
|
+
</p>
|
|
404
|
+
{/if}
|
|
405
|
+
{#if draft.endpoints.length > 1}
|
|
406
|
+
<Field.Field orientation="horizontal" class="gap-2">
|
|
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
|
+
>
|
|
415
|
+
<Select.Trigger id="kit-provider-endpoint" class="min-w-0 flex-1">
|
|
416
|
+
<span class="truncate">{endpoint?.baseUrl || t('providers.endpoint_new')}</span>
|
|
417
|
+
</Select.Trigger>
|
|
418
|
+
<Select.Content>
|
|
419
|
+
<Select.Group>
|
|
420
|
+
{#each draft.endpoints as e, index (e.slug)}
|
|
421
|
+
<Select.Item
|
|
422
|
+
value={String(index)}
|
|
423
|
+
label={e.baseUrl || t('providers.endpoint_new')}
|
|
424
|
+
/>
|
|
425
|
+
{/each}
|
|
426
|
+
</Select.Group>
|
|
427
|
+
</Select.Content>
|
|
428
|
+
</Select.Root>
|
|
429
|
+
</Field.Field>
|
|
430
|
+
{/if}
|
|
431
|
+
{#if endpoint}
|
|
432
|
+
{#key `${token}:${endpoint.slug}`}
|
|
433
|
+
<EndpointPanel
|
|
434
|
+
bind:endpoint={
|
|
435
|
+
() => endpoint,
|
|
436
|
+
(next) => {
|
|
437
|
+
if (draft) draft.endpoints[active] = next
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
check={check?.slug === endpoint.slug ? check.value : null}
|
|
441
|
+
onaddendpoint={draft.endpoints.length < 20 ? addEndpoint : null}
|
|
442
|
+
onremove={() => void removeEndpoint()}
|
|
443
|
+
oncommit={() => void commit()}
|
|
444
|
+
onkeys={keysChanged}
|
|
445
|
+
oncheck={() => void runProbe(active, false)}
|
|
446
|
+
onsync={() => void runProbe(active, true)}
|
|
447
|
+
{keyUrl}
|
|
448
|
+
{usedBy}
|
|
449
|
+
/>
|
|
450
|
+
{/key}
|
|
451
|
+
{:else}
|
|
452
|
+
<Button variant="outline" class="self-start" onclick={addEndpoint}>
|
|
453
|
+
<PlusIcon />{t('providers.add_endpoint')}
|
|
454
|
+
</Button>
|
|
455
|
+
{/if}
|
|
456
|
+
{:else}
|
|
457
|
+
<Empty.Root>
|
|
458
|
+
<Empty.Header>
|
|
459
|
+
<Empty.Title>{t('providers.empty')}</Empty.Title>
|
|
460
|
+
<Empty.Description>{t('providers.empty_hint')}</Empty.Description>
|
|
461
|
+
</Empty.Header>
|
|
462
|
+
</Empty.Root>
|
|
463
|
+
{/if}
|
|
464
|
+
</section>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<ModelPickerDialog
|
|
468
|
+
bind:open={pickerOpen}
|
|
469
|
+
result={probe?.result ?? null}
|
|
470
|
+
selected={probed?.models ?? []}
|
|
471
|
+
onchange={(models) => {
|
|
472
|
+
if (!probed) return
|
|
473
|
+
probed.models = models
|
|
474
|
+
void commit()
|
|
475
|
+
}}
|
|
476
|
+
/>
|
|
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
|
+
/>
|
|
491
|
+
{/if}
|