@ouraihub/aether-ui 0.0.0-stage → 0.1.0
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 +23 -0
- package/src/chat/ChatPanel.svelte +2097 -0
- package/src/chat/MessageItem.svelte +583 -0
- package/src/chat/SkillPicker.svelte +126 -0
- package/src/chat/ToolCallItem.svelte +23 -0
- package/src/chat/UsageDetails.svelte +138 -0
- package/src/chat/index.ts +2 -0
- package/src/chat/run-session.svelte.ts +556 -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 +45 -0
- package/src/dev/main.ts +7 -0
- package/src/i18n/en.ts +617 -0
- package/src/i18n/index.ts +23 -0
- package/src/i18n/zh-CN.ts +599 -0
- package/src/index.ts +10 -0
- package/src/logs/LogPanel.svelte +97 -0
- package/src/logs/LogSettings.svelte +56 -0
- package/src/logs/index.ts +5 -0
- package/src/logs/logger.ts +96 -0
- package/src/markdown.ts +8 -0
- package/src/mcp/McpClientSettings.svelte +491 -0
- package/src/mcp/McpServerSettings.svelte +327 -0
- package/src/mcp/config.ts +26 -0
- package/src/mcp/index.ts +3 -0
- package/src/mcp/input.ts +73 -0
- package/src/notify.svelte.ts +43 -0
- package/src/ports/contract.ts +165 -0
- package/src/ports/fake.ts +484 -0
- package/src/ports/index.ts +7 -0
- package/src/ports/search.ts +47 -0
- package/src/ports/skill-sources.ts +45 -0
- package/src/providers/EndpointKeysDialog.svelte +343 -0
- package/src/providers/EndpointPanel.svelte +491 -0
- package/src/providers/ModelPickerDialog.svelte +187 -0
- package/src/providers/ModelSelect.svelte +434 -0
- package/src/providers/ModelSettingsDialog.svelte +453 -0
- package/src/providers/ProvidersSettings.svelte +631 -0
- package/src/providers/RequestConfigDialog.svelte +250 -0
- package/src/providers/SecretInput.svelte +276 -0
- package/src/providers/draft.ts +329 -0
- package/src/providers/edit-session.ts +146 -0
- package/src/providers/index.ts +5 -0
- package/src/providers/reasoning.ts +41 -0
- package/src/providers/revision.svelte.ts +4 -0
- package/src/search/SearchSettings.svelte +703 -0
- package/src/search/index.ts +13 -0
- package/src/search/settings-flow.ts +62 -0
- package/src/session.ts +20 -0
- package/src/skills/SkillAdd.svelte +347 -0
- package/src/skills/SkillAuthoring.svelte +381 -0
- package/src/skills/SkillEditor.svelte +94 -0
- package/src/skills/SkillPreviewDialog.svelte +122 -0
- package/src/skills/SkillsSettings.svelte +140 -0
- package/src/skills/error.ts +32 -0
- package/src/skills/index.ts +4 -0
- package/src/styles/kit.css +137 -0
- package/src/styles/tokens.css +22 -0
- package/src/tauri/index.ts +282 -0
- package/src/utils.ts +22 -0
- package/src/web/index.ts +376 -0
- package/README.md +0 -3
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// One endpoint's keys, address and models.
|
|
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
|
+
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
|
+
import Settings2Icon from '@lucide/svelte/icons/settings-2'
|
|
16
|
+
import WrenchIcon from '@lucide/svelte/icons/wrench'
|
|
17
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
18
|
+
import { Switch } from '#lib/components/ui/switch/index.js'
|
|
19
|
+
import * as Dialog from '#lib/components/ui/dialog/index.js'
|
|
20
|
+
import * as Field from '#lib/components/ui/field/index.js'
|
|
21
|
+
import { Input } from '#lib/components/ui/input/index.js'
|
|
22
|
+
import CopyInput from '../CopyInput.svelte'
|
|
23
|
+
import * as Select from '#lib/components/ui/select/index.js'
|
|
24
|
+
import { getKitContext } from '../context'
|
|
25
|
+
import type { KeyStrategy, Model, Protocol, Provider } from '../ports'
|
|
26
|
+
import { cn } from '../utils'
|
|
27
|
+
import { groupKey, keyError, maskedText, probeUrl, validUrl, type EndpointDraft } from './draft'
|
|
28
|
+
import ModelSettingsDialog from './ModelSettingsDialog.svelte'
|
|
29
|
+
import RequestConfigDialog from './RequestConfigDialog.svelte'
|
|
30
|
+
import { settingsTarget } from './revision.svelte'
|
|
31
|
+
import SecretInput from './SecretInput.svelte'
|
|
32
|
+
|
|
33
|
+
export type CheckOutcome =
|
|
34
|
+
{ state: 'ok'; count: number; suggested: Protocol | null } | { state: 'error'; message: string }
|
|
35
|
+
export type CheckState =
|
|
36
|
+
| { state: 'running' }
|
|
37
|
+
| CheckOutcome
|
|
38
|
+
/** One probe per enabled key when there are at least two (docs/34 §5.3). */
|
|
39
|
+
| { state: 'keys'; results: { keyId: string; value: CheckOutcome }[] }
|
|
40
|
+
|
|
41
|
+
let {
|
|
42
|
+
endpoint = $bindable(),
|
|
43
|
+
check,
|
|
44
|
+
onaddendpoint,
|
|
45
|
+
onremove,
|
|
46
|
+
oncommit,
|
|
47
|
+
onkeys,
|
|
48
|
+
oncheck,
|
|
49
|
+
onsync,
|
|
50
|
+
keyUrl,
|
|
51
|
+
usedBy,
|
|
52
|
+
}: {
|
|
53
|
+
endpoint: EndpointDraft
|
|
54
|
+
check: CheckState | null
|
|
55
|
+
onaddendpoint: (() => void) | null
|
|
56
|
+
onremove: () => void
|
|
57
|
+
oncommit: () => void
|
|
58
|
+
/** Providers returned by a key operation. */
|
|
59
|
+
onkeys: (providers: Provider[]) => void
|
|
60
|
+
oncheck: () => void
|
|
61
|
+
onsync: () => void
|
|
62
|
+
/** Preset "get a key" link shown beside the label (docs/39). */
|
|
63
|
+
keyUrl?: string | undefined
|
|
64
|
+
/** Localized roles bound to a stored model (contract §6). */
|
|
65
|
+
usedBy?: ((endpointId: string, modelId: string) => string[]) | undefined
|
|
66
|
+
} = $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
|
+
const { ports, i18n, notify } = getKitContext()
|
|
77
|
+
const { t } = i18n
|
|
78
|
+
let keySaving = $state(false)
|
|
79
|
+
async function setKeyEnabled(keyId: string, enabled: boolean) {
|
|
80
|
+
const endpointId = endpoint.saved?.id
|
|
81
|
+
if (!ports.providers || !endpointId || keySaving) return
|
|
82
|
+
keySaving = true
|
|
83
|
+
try {
|
|
84
|
+
onkeys(await ports.providers.saveKey({ endpointId, id: keyId, enabled }))
|
|
85
|
+
} catch (error) {
|
|
86
|
+
notify.toast('error', keyError(error, t))
|
|
87
|
+
} finally {
|
|
88
|
+
keySaving = false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let url = $state(endpoint.baseUrl)
|
|
93
|
+
let urlError = $state(false)
|
|
94
|
+
// A save replaces the draft object; only a changed committed address may overwrite what the user is typing.
|
|
95
|
+
let seenUrl = endpoint.baseUrl
|
|
96
|
+
$effect.pre(() => {
|
|
97
|
+
const next = endpoint.baseUrl
|
|
98
|
+
if (next === seenUrl) return
|
|
99
|
+
seenUrl = next
|
|
100
|
+
url = next
|
|
101
|
+
urlError = false
|
|
102
|
+
})
|
|
103
|
+
let settingsOpen = $state(false)
|
|
104
|
+
// A chat "open settings" action names this endpoint's model: open its dialog once.
|
|
105
|
+
const targeted = settingsTarget.value?.endpointId === endpoint.saved?.id
|
|
106
|
+
const targetModel = targeted
|
|
107
|
+
? (endpoint.models.find((model) => model.id === settingsTarget.value?.modelId) ?? null)
|
|
108
|
+
: null
|
|
109
|
+
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
|
+
function commitUrl() {
|
|
139
|
+
const next = url.trim()
|
|
140
|
+
if (next === endpoint.baseUrl) return
|
|
141
|
+
if (next && !validUrl(next)) {
|
|
142
|
+
urlError = true
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
urlError = false
|
|
146
|
+
endpoint.baseUrl = next
|
|
147
|
+
oncommit()
|
|
148
|
+
}
|
|
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
|
+
/** First probe suggestion: the whole check or any key's (all keys share one relay). */
|
|
178
|
+
const suggested = $derived.by((): Protocol | null => {
|
|
179
|
+
if (check?.state === 'ok') return check.suggested
|
|
180
|
+
if (check?.state !== 'keys') return null
|
|
181
|
+
for (const { value } of check.results) if (value.state === 'ok' && value.suggested) return value.suggested
|
|
182
|
+
return null
|
|
183
|
+
})
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<div class="flex min-w-0 flex-col gap-5">
|
|
187
|
+
<Field.Field class="gap-2">
|
|
188
|
+
<div class="flex items-center gap-2">
|
|
189
|
+
<Field.Label for="kit-endpoint-key">{t('providers.api_key')}</Field.Label>
|
|
190
|
+
{#if keyUrl && /^https:\/\//.test(keyUrl)}<a
|
|
191
|
+
class="text-primary text-kit-xs hover:underline"
|
|
192
|
+
href={keyUrl}
|
|
193
|
+
target="_blank"
|
|
194
|
+
rel="noopener noreferrer">{t('providers.get_key')}</a
|
|
195
|
+
>{/if}
|
|
196
|
+
</div>
|
|
197
|
+
<div class="flex min-w-0 items-start gap-2">
|
|
198
|
+
<SecretInput id="kit-endpoint-key" endpoint={endpoint.saved} onchanged={onkeys} />
|
|
199
|
+
<Button variant="outline" class="shrink-0" disabled={!validUrl(endpoint.baseUrl)} onclick={oncheck}>
|
|
200
|
+
{#if check?.state === 'running'}<LoaderIcon class="animate-spin" />{/if}
|
|
201
|
+
{t('providers.check')}
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
204
|
+
{#if check?.state === 'keys'}
|
|
205
|
+
<ul role="status" class="flex min-w-0 flex-col gap-1">
|
|
206
|
+
{#each check.results as { keyId, value } (keyId)}
|
|
207
|
+
{@const key = endpoint.saved?.keys.find((k) => k.id === keyId)}
|
|
208
|
+
{#if key}
|
|
209
|
+
<li class="flex min-w-0 items-start gap-2 text-kit-xs">
|
|
210
|
+
<span
|
|
211
|
+
class={cn(
|
|
212
|
+
'min-w-0 flex-1 whitespace-pre-line',
|
|
213
|
+
value.state === 'ok' ? 'text-success' : 'text-destructive',
|
|
214
|
+
)}
|
|
215
|
+
>{value.state === 'ok' ? '✓' : '✕'}
|
|
216
|
+
{key.label || maskedText(key, t)} — {value.state === 'ok'
|
|
217
|
+
? t('providers.check_ok', { count: value.count })
|
|
218
|
+
: value.message}</span
|
|
219
|
+
>
|
|
220
|
+
<Switch
|
|
221
|
+
size="sm"
|
|
222
|
+
aria-label={t('providers.keys_enable_for', { name: key.label || maskedText(key, t) })}
|
|
223
|
+
checked={key.enabled}
|
|
224
|
+
disabled={keySaving}
|
|
225
|
+
onCheckedChange={(enabled) => void setKeyEnabled(key.id, enabled)}
|
|
226
|
+
/>
|
|
227
|
+
</li>
|
|
228
|
+
{/if}
|
|
229
|
+
{/each}
|
|
230
|
+
</ul>
|
|
231
|
+
{:else if check && check.state !== 'running'}
|
|
232
|
+
<p
|
|
233
|
+
role="status"
|
|
234
|
+
class={cn('text-kit-xs whitespace-pre-line', check.state === 'ok' ? 'text-success' : 'text-destructive')}
|
|
235
|
+
>
|
|
236
|
+
{check.state === 'ok' ? t('providers.check_ok', { count: check.count }) : check.message}
|
|
237
|
+
</p>
|
|
238
|
+
{/if}
|
|
239
|
+
{#if suggested && suggested !== endpoint.protocol}
|
|
240
|
+
{@const next = suggested}
|
|
241
|
+
<div role="status" class="bg-info/10 flex min-w-0 items-center gap-2 rounded-md px-2.5 py-1.5 text-kit-xs">
|
|
242
|
+
<span class="min-w-0 flex-1">{t('providers.suggest_protocol')}</span>
|
|
243
|
+
<Button
|
|
244
|
+
variant="outline"
|
|
245
|
+
size="sm"
|
|
246
|
+
class="shrink-0"
|
|
247
|
+
onclick={() => {
|
|
248
|
+
endpoint.protocol = next
|
|
249
|
+
oncommit()
|
|
250
|
+
}}>{t('providers.suggest_protocol_apply')}</Button
|
|
251
|
+
>
|
|
252
|
+
</div>
|
|
253
|
+
{/if}
|
|
254
|
+
</Field.Field>
|
|
255
|
+
|
|
256
|
+
{#if ports.providers?.keyStrategies}
|
|
257
|
+
<Field.Field class="gap-2">
|
|
258
|
+
<Field.Label for="kit-endpoint-key-strategy">{t('providers.key_strategy')}</Field.Label>
|
|
259
|
+
<Select.Root
|
|
260
|
+
type="single"
|
|
261
|
+
value={endpoint.keyStrategy}
|
|
262
|
+
onValueChange={(value) => {
|
|
263
|
+
if (value === endpoint.keyStrategy) return
|
|
264
|
+
endpoint.keyStrategy = value as KeyStrategy
|
|
265
|
+
oncommit()
|
|
266
|
+
}}
|
|
267
|
+
>
|
|
268
|
+
<Select.Trigger id="kit-endpoint-key-strategy" class="w-full sm:w-64"
|
|
269
|
+
>{t(`providers.key_strategy_${endpoint.keyStrategy}`)}</Select.Trigger
|
|
270
|
+
>
|
|
271
|
+
<Select.Content>
|
|
272
|
+
<Select.Group>
|
|
273
|
+
{#each ['round_robin', 'fill_first'] as const as strategy (strategy)}<Select.Item
|
|
274
|
+
value={strategy}
|
|
275
|
+
label={t(`providers.key_strategy_${strategy}`)}
|
|
276
|
+
/>{/each}
|
|
277
|
+
</Select.Group>
|
|
278
|
+
</Select.Content>
|
|
279
|
+
</Select.Root>
|
|
280
|
+
<Field.Description class="text-kit-xs"
|
|
281
|
+
>{t(`providers.key_strategy_${endpoint.keyStrategy}_desc`)}</Field.Description
|
|
282
|
+
>
|
|
283
|
+
</Field.Field>
|
|
284
|
+
{/if}
|
|
285
|
+
|
|
286
|
+
<Field.Field class="gap-2">
|
|
287
|
+
<div class="flex items-center gap-2">
|
|
288
|
+
<Field.Label for="kit-endpoint-url">{t('providers.base_url')}</Field.Label>
|
|
289
|
+
{#if onaddendpoint}
|
|
290
|
+
<button type="button" class="text-primary text-kit-xs hover:underline" onclick={onaddendpoint}>
|
|
291
|
+
{t('providers.add_endpoint')}
|
|
292
|
+
</button>
|
|
293
|
+
{/if}
|
|
294
|
+
</div>
|
|
295
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
296
|
+
<CopyInput
|
|
297
|
+
id="kit-endpoint-url"
|
|
298
|
+
class="min-w-0 flex-1 font-mono"
|
|
299
|
+
placeholder="https://api.example.com/v1"
|
|
300
|
+
aria-invalid={urlError}
|
|
301
|
+
bind:value={url}
|
|
302
|
+
onblur={commitUrl}
|
|
303
|
+
onkeydown={(event) => event.key === 'Enter' && event.currentTarget.blur()}
|
|
304
|
+
/>
|
|
305
|
+
<Button
|
|
306
|
+
variant="outline"
|
|
307
|
+
size="icon"
|
|
308
|
+
class="shrink-0"
|
|
309
|
+
title={t('providers.request_config_tip')}
|
|
310
|
+
aria-label={t('providers.request_config_tip')}
|
|
311
|
+
onclick={() => (settingsOpen = true)}><Settings2Icon /></Button
|
|
312
|
+
>
|
|
313
|
+
</div>
|
|
314
|
+
{#if urlError}
|
|
315
|
+
<p role="alert" class="text-destructive text-kit-xs">{t('providers.url_invalid')}</p>
|
|
316
|
+
{:else if validUrl(endpoint.baseUrl)}
|
|
317
|
+
<p class="text-muted-foreground truncate font-mono text-kit-xs">GET {probeUrl(endpoint.baseUrl)}</p>
|
|
318
|
+
{/if}
|
|
319
|
+
</Field.Field>
|
|
320
|
+
|
|
321
|
+
<section class="flex min-w-0 flex-col gap-2" aria-labelledby="kit-endpoint-models">
|
|
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>
|
|
457
|
+
</div>
|
|
458
|
+
|
|
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
|
+
<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>
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Search and filter fetched models; every +/- applies at once.
|
|
3
|
+
import { untrack } from 'svelte'
|
|
4
|
+
import ChevronRight from '@lucide/svelte/icons/chevron-right'
|
|
5
|
+
import Plus from '@lucide/svelte/icons/plus'
|
|
6
|
+
import Minus from '@lucide/svelte/icons/minus'
|
|
7
|
+
import { Button } from '#lib/components/ui/button/index.js'
|
|
8
|
+
import { Input } from '#lib/components/ui/input/index.js'
|
|
9
|
+
import * as Dialog from '#lib/components/ui/dialog/index.js'
|
|
10
|
+
import * as Select from '#lib/components/ui/select/index.js'
|
|
11
|
+
import { getKitContext } from '../context'
|
|
12
|
+
import type { Model, ModelType, ProbeResult } from '../ports'
|
|
13
|
+
import { mergeModels } from './draft'
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
open = $bindable(false),
|
|
17
|
+
result,
|
|
18
|
+
selected,
|
|
19
|
+
onchange,
|
|
20
|
+
}: {
|
|
21
|
+
open: boolean
|
|
22
|
+
result: ProbeResult | null
|
|
23
|
+
/** Currently selected models; preselected and their user-set types kept. */
|
|
24
|
+
selected: Model[]
|
|
25
|
+
/** Called after every change with the endpoint's full model list. */
|
|
26
|
+
onchange: (models: Model[]) => void
|
|
27
|
+
} = $props()
|
|
28
|
+
|
|
29
|
+
const TYPES: ModelType[] = ['chat', 'image', 'video', 'tts', 'stt', 'embedding', 'rerank', 'unknown']
|
|
30
|
+
const { t } = getKitContext().i18n
|
|
31
|
+
|
|
32
|
+
let checked = $state<Record<string, boolean>>({})
|
|
33
|
+
let types = $state<Record<string, ModelType>>({})
|
|
34
|
+
let query = $state('')
|
|
35
|
+
let filter = $state<ModelType | 'all'>('all')
|
|
36
|
+
let collapsed = $state<Record<string, boolean>>({})
|
|
37
|
+
|
|
38
|
+
$effect.pre(() => {
|
|
39
|
+
if (!open || !result) return
|
|
40
|
+
const current = Object.fromEntries(untrack(() => selected).map((m) => [m.id, m]))
|
|
41
|
+
checked = Object.fromEntries(result.models.map((m) => [m.id, m.id in current]))
|
|
42
|
+
types = Object.fromEntries(result.models.map((m) => [m.id, current[m.id]?.modelType ?? m.modelType]))
|
|
43
|
+
query = ''
|
|
44
|
+
filter = 'all'
|
|
45
|
+
collapsed = {}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const models = $derived(result?.models ?? [])
|
|
49
|
+
const searched = $derived(models.filter((m) => m.id.toLowerCase().includes(query.trim().toLowerCase())))
|
|
50
|
+
const shown = $derived(searched.filter((m) => filter === 'all' || (types[m.id] ?? m.modelType) === filter))
|
|
51
|
+
const groups = $derived(Object.entries(Object.groupBy(shown, (m) => m.group ?? '')))
|
|
52
|
+
const count = $derived(models.filter((m) => checked[m.id]).length)
|
|
53
|
+
const bulkRemoves = $derived(shown.length > 0 && shown.every((m) => checked[m.id]))
|
|
54
|
+
|
|
55
|
+
function bulk() {
|
|
56
|
+
const on = !bulkRemoves
|
|
57
|
+
for (const model of shown) checked[model.id] = on
|
|
58
|
+
emit()
|
|
59
|
+
}
|
|
60
|
+
function toggle(id: string) {
|
|
61
|
+
checked[id] = !checked[id]
|
|
62
|
+
emit()
|
|
63
|
+
}
|
|
64
|
+
function setType(id: string, type: ModelType) {
|
|
65
|
+
types[id] = type
|
|
66
|
+
if (checked[id]) emit()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Models outside this probe (added by hand, or no longer listed) are kept untouched. */
|
|
70
|
+
function emit() {
|
|
71
|
+
onchange(mergeModels(selected, models, checked, types))
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<Dialog.Root bind:open>
|
|
76
|
+
<Dialog.Content class="flex max-h-[85dvh] min-w-0 flex-col gap-3 sm:max-w-2xl">
|
|
77
|
+
<Dialog.Header class="shrink-0 text-left">
|
|
78
|
+
<Dialog.Title class="flex items-center gap-2 pr-6">
|
|
79
|
+
{t('providers.picker_title')}
|
|
80
|
+
<span aria-hidden="true" class="bg-muted rounded px-1.5 text-kit-xs tabular-nums">{models.length}</span>
|
|
81
|
+
</Dialog.Title>
|
|
82
|
+
<Dialog.Description>{t('providers.picker_hint')}</Dialog.Description>
|
|
83
|
+
</Dialog.Header>
|
|
84
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
85
|
+
<Input
|
|
86
|
+
class="min-w-0 flex-1"
|
|
87
|
+
bind:value={query}
|
|
88
|
+
placeholder={t('providers.search_models')}
|
|
89
|
+
aria-label={t('providers.search_models')}
|
|
90
|
+
autocomplete="off"
|
|
91
|
+
spellcheck={false}
|
|
92
|
+
/>
|
|
93
|
+
<Button variant="outline" size="sm" class="shrink-0" disabled={shown.length === 0} onclick={bulk}>
|
|
94
|
+
{t(bulkRemoves ? 'providers.remove_all' : 'providers.add_all')}
|
|
95
|
+
</Button>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="flex shrink-0 gap-1 overflow-x-auto pb-1" role="group" aria-label={t('providers.model_type')}>
|
|
98
|
+
{#each ['all', ...TYPES] as type (type)}
|
|
99
|
+
{@const hits =
|
|
100
|
+
type === 'all' ? searched.length : searched.filter((m) => (types[m.id] ?? m.modelType) === type).length}
|
|
101
|
+
<Button
|
|
102
|
+
variant={filter === type ? 'secondary' : 'ghost'}
|
|
103
|
+
size="sm"
|
|
104
|
+
class="shrink-0"
|
|
105
|
+
aria-pressed={filter === type}
|
|
106
|
+
disabled={hits === 0 && type !== 'all'}
|
|
107
|
+
onclick={() => (filter = type as ModelType | 'all')}
|
|
108
|
+
>
|
|
109
|
+
{type === 'all' ? t('providers.all_types') : t(`providers.type.${type as ModelType}`)}
|
|
110
|
+
<span class="text-muted-foreground tabular-nums">{hits}</span>
|
|
111
|
+
</Button>
|
|
112
|
+
{/each}
|
|
113
|
+
</div>
|
|
114
|
+
<div class="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto" role="region" aria-label={t('providers.models')}>
|
|
115
|
+
{#if shown.length === 0}
|
|
116
|
+
<p class="text-muted-foreground py-8 text-center text-kit-sm">{t('providers.no_model_matches')}</p>
|
|
117
|
+
{/if}
|
|
118
|
+
{#each groups as [group, items = []] (group)}
|
|
119
|
+
{@const folded = !query.trim() && collapsed[group]}
|
|
120
|
+
<section class="flex min-w-0 flex-col">
|
|
121
|
+
<Button
|
|
122
|
+
variant="ghost"
|
|
123
|
+
size="sm"
|
|
124
|
+
class="justify-start"
|
|
125
|
+
aria-expanded={!folded}
|
|
126
|
+
onclick={() => (collapsed[group] = !collapsed[group])}
|
|
127
|
+
>
|
|
128
|
+
<ChevronRight class={folded ? '' : 'rotate-90'} />
|
|
129
|
+
{group || t('providers.no_group')}
|
|
130
|
+
<span class="bg-muted rounded px-1.5 tabular-nums">{items.length}</span>
|
|
131
|
+
</Button>
|
|
132
|
+
{#if !folded}
|
|
133
|
+
<ul>
|
|
134
|
+
{#each items as model (model.id)}
|
|
135
|
+
<li class="hover:bg-accent/50 flex min-w-0 items-center gap-1 rounded-md py-0.5 pl-4">
|
|
136
|
+
<span class="min-w-0 flex-1 truncate font-mono text-kit-sm" title={model.id}>{model.id}</span>
|
|
137
|
+
<Select.Root
|
|
138
|
+
type="single"
|
|
139
|
+
value={types[model.id] ?? model.modelType}
|
|
140
|
+
onValueChange={(value) => setType(model.id, value as ModelType)}
|
|
141
|
+
>
|
|
142
|
+
<Select.Trigger
|
|
143
|
+
size="sm"
|
|
144
|
+
class="shrink-0 border-0 bg-transparent shadow-none"
|
|
145
|
+
aria-label={t('providers.type_for', { id: model.id })}
|
|
146
|
+
>
|
|
147
|
+
{t(`providers.type.${types[model.id] ?? model.modelType}`)}
|
|
148
|
+
</Select.Trigger>
|
|
149
|
+
<Select.Content
|
|
150
|
+
><Select.Group>
|
|
151
|
+
{#each TYPES as type (type)}
|
|
152
|
+
<Select.Item value={type} label={t(`providers.type.${type}`)} />
|
|
153
|
+
{/each}
|
|
154
|
+
</Select.Group></Select.Content
|
|
155
|
+
>
|
|
156
|
+
</Select.Root>
|
|
157
|
+
{#if model.canVision}<span class="text-muted-foreground hidden shrink-0 text-kit-xs sm:inline"
|
|
158
|
+
>{t('providers.vision')}</span
|
|
159
|
+
>{/if}
|
|
160
|
+
<span class="text-success w-8 shrink-0 text-right text-kit-xs"
|
|
161
|
+
>{checked[model.id] ? t('providers.picked') : ''}</span
|
|
162
|
+
>
|
|
163
|
+
<Button
|
|
164
|
+
variant="ghost"
|
|
165
|
+
size="icon-sm"
|
|
166
|
+
class="shrink-0"
|
|
167
|
+
aria-label={t(checked[model.id] ? 'providers.remove_model' : 'providers.add_model', {
|
|
168
|
+
id: model.id,
|
|
169
|
+
})}
|
|
170
|
+
onclick={() => toggle(model.id)}
|
|
171
|
+
>
|
|
172
|
+
{#if checked[model.id]}<Minus />{:else}<Plus />{/if}
|
|
173
|
+
</Button>
|
|
174
|
+
</li>
|
|
175
|
+
{/each}
|
|
176
|
+
</ul>
|
|
177
|
+
{/if}
|
|
178
|
+
</section>
|
|
179
|
+
{/each}
|
|
180
|
+
</div>
|
|
181
|
+
<Dialog.Footer class="shrink-0">
|
|
182
|
+
<p class="text-muted-foreground mr-auto text-kit-xs" aria-live="polite">
|
|
183
|
+
{t('providers.picked_count', { count })}
|
|
184
|
+
</p>
|
|
185
|
+
</Dialog.Footer>
|
|
186
|
+
</Dialog.Content>
|
|
187
|
+
</Dialog.Root>
|