@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.
Files changed (212) hide show
  1. package/LICENSE +789 -0
  2. package/fixtures/model-classify.json +42 -0
  3. package/fixtures/pricing-cost.json +56 -0
  4. package/fixtures/reasoning-infer.json +44 -0
  5. package/fixtures/sse/anthropic-messages/chinese.sse +24 -0
  6. package/fixtures/sse/anthropic-messages/multi-tool.sse +33 -0
  7. package/fixtures/sse/anthropic-messages/single-tool.sse +39 -0
  8. package/fixtures/sse/anthropic-messages/text.sse +27 -0
  9. package/fixtures/sse/anthropic-messages/truncated.sse +21 -0
  10. package/fixtures/sse/anthropic-messages/upstream-error.sse +12 -0
  11. package/fixtures/sse/expect.json +20 -0
  12. package/fixtures/sse/openai-chat/chinese.sse +14 -0
  13. package/fixtures/sse/openai-chat/multi-tool.sse +18 -0
  14. package/fixtures/sse/openai-chat/single-tool.sse +22 -0
  15. package/fixtures/sse/openai-chat/text.sse +14 -0
  16. package/fixtures/sse/openai-chat/truncated.sse +12 -0
  17. package/fixtures/sse/openai-chat/upstream-error.sse +6 -0
  18. package/fixtures/sse/openai-responses/chinese.sse +33 -0
  19. package/fixtures/sse/openai-responses/multi-tool.sse +36 -0
  20. package/fixtures/sse/openai-responses/single-tool.sse +27 -0
  21. package/fixtures/sse/openai-responses/text.sse +33 -0
  22. package/fixtures/sse/openai-responses/truncated.sse +30 -0
  23. package/fixtures/sse/openai-responses/upstream-error.sse +9 -0
  24. package/package.json +71 -3
  25. package/src/CopyInput.svelte +33 -0
  26. package/src/KitNotifier.svelte +33 -0
  27. package/src/chat/ApprovalCard.svelte +29 -0
  28. package/src/chat/ChatComposer.svelte +277 -0
  29. package/src/chat/ChatMessage.svelte +262 -0
  30. package/src/chat/ChatPanel.css +386 -0
  31. package/src/chat/ChatPanel.svelte +496 -0
  32. package/src/chat/ChatPanelApprovals.svelte +28 -0
  33. package/src/chat/ChatPanelComposer.svelte +317 -0
  34. package/src/chat/ChatPanelDialogs.svelte +242 -0
  35. package/src/chat/ChatPanelHeader.svelte +132 -0
  36. package/src/chat/ChatPanelMessages.svelte +166 -0
  37. package/src/chat/ChatPanelMode.svelte +190 -0
  38. package/src/chat/ChatPanelState.svelte.ts +1 -0
  39. package/src/chat/ChatPanelToolbar.svelte +425 -0
  40. package/src/chat/ChatSurface.svelte +168 -0
  41. package/src/chat/MessageItem.svelte +480 -0
  42. package/src/chat/ReasoningControl.svelte +286 -0
  43. package/src/chat/SkillPicker.svelte +133 -0
  44. package/src/chat/ToolCallItem.svelte +23 -0
  45. package/src/chat/UsageControl.svelte +216 -0
  46. package/src/chat/UsageDetails.svelte +76 -0
  47. package/src/chat/chat-attachments.svelte.ts +74 -0
  48. package/src/chat/chat-panel-history.svelte.ts +430 -0
  49. package/src/chat/chat-panel-props.ts +32 -0
  50. package/src/chat/index.ts +6 -0
  51. package/src/chat/run-session-history.ts +56 -0
  52. package/src/chat/run-session-types.ts +69 -0
  53. package/src/chat/run-session.svelte.ts +494 -0
  54. package/src/chat/test-support/chat-fixture.ts +178 -0
  55. package/src/chat/test-support/conversation-fixtures.ts +32 -0
  56. package/src/chat/test-support/run-session-fixture.ts +112 -0
  57. package/src/components/ui/alert/alert-action.svelte +20 -0
  58. package/src/components/ui/alert/alert-description.svelte +23 -0
  59. package/src/components/ui/alert/alert-title.svelte +23 -0
  60. package/src/components/ui/alert/alert.svelte +43 -0
  61. package/src/components/ui/alert/index.ts +17 -0
  62. package/src/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
  63. package/src/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
  64. package/src/components/ui/alert-dialog/alert-dialog-content.svelte +32 -0
  65. package/src/components/ui/alert-dialog/alert-dialog-description.svelte +17 -0
  66. package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
  67. package/src/components/ui/alert-dialog/alert-dialog-header.svelte +20 -0
  68. package/src/components/ui/alert-dialog/alert-dialog-media.svelte +20 -0
  69. package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +17 -0
  70. package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
  71. package/src/components/ui/alert-dialog/alert-dialog-title.svelte +17 -0
  72. package/src/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
  73. package/src/components/ui/alert-dialog/alert-dialog.svelte +7 -0
  74. package/src/components/ui/alert-dialog/index.ts +40 -0
  75. package/src/components/ui/button/button.svelte +81 -0
  76. package/src/components/ui/button/index.ts +17 -0
  77. package/src/components/ui/dialog/dialog-close.svelte +11 -0
  78. package/src/components/ui/dialog/dialog-content.svelte +48 -0
  79. package/src/components/ui/dialog/dialog-description.svelte +17 -0
  80. package/src/components/ui/dialog/dialog-footer.svelte +32 -0
  81. package/src/components/ui/dialog/dialog-header.svelte +20 -0
  82. package/src/components/ui/dialog/dialog-overlay.svelte +17 -0
  83. package/src/components/ui/dialog/dialog-portal.svelte +7 -0
  84. package/src/components/ui/dialog/dialog-title.svelte +17 -0
  85. package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
  86. package/src/components/ui/dialog/dialog.svelte +7 -0
  87. package/src/components/ui/dialog/index.ts +34 -0
  88. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
  89. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +46 -0
  90. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
  91. package/src/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
  92. package/src/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
  93. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
  94. package/src/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
  95. package/src/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
  96. package/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +17 -0
  97. package/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +36 -0
  98. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  99. package/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
  100. package/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +31 -0
  101. package/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
  102. package/src/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
  103. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
  104. package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  105. package/src/components/ui/dropdown-menu/index.ts +54 -0
  106. package/src/components/ui/empty/empty-content.svelte +23 -0
  107. package/src/components/ui/empty/empty-description.svelte +23 -0
  108. package/src/components/ui/empty/empty-header.svelte +20 -0
  109. package/src/components/ui/empty/empty-media.svelte +41 -0
  110. package/src/components/ui/empty/empty-title.svelte +20 -0
  111. package/src/components/ui/empty/empty.svelte +23 -0
  112. package/src/components/ui/empty/index.ts +22 -0
  113. package/src/components/ui/field/field-content.svelte +20 -0
  114. package/src/components/ui/field/field-description.svelte +25 -0
  115. package/src/components/ui/field/field-error.svelte +58 -0
  116. package/src/components/ui/field/field-group.svelte +23 -0
  117. package/src/components/ui/field/field-label.svelte +25 -0
  118. package/src/components/ui/field/field-legend.svelte +24 -0
  119. package/src/components/ui/field/field-separator.svelte +35 -0
  120. package/src/components/ui/field/field-set.svelte +20 -0
  121. package/src/components/ui/field/field-title.svelte +20 -0
  122. package/src/components/ui/field/field.svelte +47 -0
  123. package/src/components/ui/field/index.ts +33 -0
  124. package/src/components/ui/input/index.ts +7 -0
  125. package/src/components/ui/input/input.svelte +48 -0
  126. package/src/components/ui/label/index.ts +7 -0
  127. package/src/components/ui/label/label.svelte +20 -0
  128. package/src/components/ui/select/index.ts +40 -0
  129. package/src/components/ui/select/select-content.svelte +45 -0
  130. package/src/components/ui/select/select-group-heading.svelte +21 -0
  131. package/src/components/ui/select/select-group.svelte +17 -0
  132. package/src/components/ui/select/select-item.svelte +43 -0
  133. package/src/components/ui/select/select-label.svelte +20 -0
  134. package/src/components/ui/select/select-portal.svelte +7 -0
  135. package/src/components/ui/select/select-scroll-down-button.svelte +20 -0
  136. package/src/components/ui/select/select-scroll-up-button.svelte +20 -0
  137. package/src/components/ui/select/select-separator.svelte +18 -0
  138. package/src/components/ui/select/select-trigger.svelte +29 -0
  139. package/src/components/ui/select/select-value.svelte +7 -0
  140. package/src/components/ui/select/select.svelte +11 -0
  141. package/src/components/ui/separator/index.ts +7 -0
  142. package/src/components/ui/separator/separator.svelte +21 -0
  143. package/src/components/ui/sonner/index.ts +1 -0
  144. package/src/components/ui/sonner/sonner.svelte +40 -0
  145. package/src/components/ui/switch/index.ts +7 -0
  146. package/src/components/ui/switch/switch.svelte +34 -0
  147. package/src/components/ui/textarea/index.ts +7 -0
  148. package/src/components/ui/textarea/textarea.svelte +22 -0
  149. package/src/context.ts +34 -0
  150. package/src/dev/Demo.svelte +47 -0
  151. package/src/dev/main.ts +7 -0
  152. package/src/i18n/en.ts +645 -0
  153. package/src/i18n/index.ts +28 -0
  154. package/src/i18n/zh-CN.ts +610 -0
  155. package/src/index.ts +10 -0
  156. package/src/logs/LogPanel.svelte +103 -0
  157. package/src/logs/LogSettings.svelte +58 -0
  158. package/src/logs/index.ts +5 -0
  159. package/src/logs/logger.ts +96 -0
  160. package/src/markdown.ts +8 -0
  161. package/src/mcp/McpClientEditor.svelte +204 -0
  162. package/src/mcp/McpClientSettings.svelte +418 -0
  163. package/src/mcp/McpServerSettings.svelte +363 -0
  164. package/src/mcp/config.ts +26 -0
  165. package/src/mcp/index.ts +3 -0
  166. package/src/mcp/input.ts +74 -0
  167. package/src/notify.svelte.ts +43 -0
  168. package/src/ports/contract.ts +174 -0
  169. package/src/ports/fake-optional.ts +81 -0
  170. package/src/ports/fake.ts +488 -0
  171. package/src/ports/index.ts +7 -0
  172. package/src/ports/search.ts +56 -0
  173. package/src/ports/skill-sources.ts +45 -0
  174. package/src/ports/test-support/fake-fixture.ts +63 -0
  175. package/src/providers/EndpointKeysDialog.svelte +357 -0
  176. package/src/providers/EndpointModels.svelte +311 -0
  177. package/src/providers/EndpointPanel.svelte +266 -0
  178. package/src/providers/ModelPickerDialog.svelte +230 -0
  179. package/src/providers/ModelPricing.svelte +125 -0
  180. package/src/providers/ModelSelect.svelte +448 -0
  181. package/src/providers/ModelSettingsDialog.svelte +388 -0
  182. package/src/providers/ProviderDialogs.svelte +120 -0
  183. package/src/providers/ProviderList.svelte +159 -0
  184. package/src/providers/ProvidersSettings.svelte +491 -0
  185. package/src/providers/RequestConfigDialog.svelte +278 -0
  186. package/src/providers/SecretInput.svelte +284 -0
  187. package/src/providers/draft.ts +355 -0
  188. package/src/providers/edit-session.ts +170 -0
  189. package/src/providers/index.ts +5 -0
  190. package/src/providers/reasoning.ts +55 -0
  191. package/src/providers/revision.svelte.ts +6 -0
  192. package/src/providers/test-support/providers-fixture.ts +120 -0
  193. package/src/search/SearchAdvancedTools.svelte +182 -0
  194. package/src/search/SearchProviderCard.svelte +195 -0
  195. package/src/search/SearchSettings.svelte +500 -0
  196. package/src/search/index.ts +13 -0
  197. package/src/search/settings-flow.ts +67 -0
  198. package/src/session.ts +20 -0
  199. package/src/skills/SkillAdd.svelte +380 -0
  200. package/src/skills/SkillAuthoring.svelte +430 -0
  201. package/src/skills/SkillEditor.svelte +104 -0
  202. package/src/skills/SkillPreviewDialog.svelte +136 -0
  203. package/src/skills/SkillsSettings.svelte +147 -0
  204. package/src/skills/error.ts +32 -0
  205. package/src/skills/index.ts +4 -0
  206. package/src/skills/test-support/skills-fixture.ts +139 -0
  207. package/src/styles/kit.css +137 -0
  208. package/src/styles/tokens.css +22 -0
  209. package/src/tauri/index.ts +298 -0
  210. package/src/utils.ts +22 -0
  211. package/src/web/index.ts +441 -0
  212. package/README.md +0 -3
@@ -0,0 +1,230 @@
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[] = [
30
+ 'chat',
31
+ 'image',
32
+ 'video',
33
+ 'tts',
34
+ 'stt',
35
+ 'embedding',
36
+ 'rerank',
37
+ 'unknown',
38
+ ]
39
+ const { t } = getKitContext().i18n
40
+
41
+ let checked = $state<Record<string, boolean>>({})
42
+ let types = $state<Record<string, ModelType>>({})
43
+ let query = $state('')
44
+ let filter = $state<ModelType | 'all'>('all')
45
+ let collapsed = $state<Record<string, boolean>>({})
46
+
47
+ $effect.pre(() => {
48
+ if (!open || !result) return
49
+ const current = Object.fromEntries(untrack(() => selected).map((m) => [m.id, m]))
50
+ checked = Object.fromEntries(result.models.map((m) => [m.id, m.id in current]))
51
+ types = Object.fromEntries(
52
+ result.models.map((m) => [m.id, current[m.id]?.modelType ?? m.modelType]),
53
+ )
54
+ query = ''
55
+ filter = 'all'
56
+ collapsed = {}
57
+ })
58
+
59
+ const models = $derived(result?.models ?? [])
60
+ const searched = $derived(
61
+ models.filter((m) => m.id.toLowerCase().includes(query.trim().toLowerCase())),
62
+ )
63
+ const shown = $derived(
64
+ searched.filter((m) => filter === 'all' || (types[m.id] ?? m.modelType) === filter),
65
+ )
66
+ const groups = $derived(Object.entries(Object.groupBy(shown, (m) => m.group ?? '')))
67
+ const count = $derived(models.filter((m) => checked[m.id]).length)
68
+ const bulkRemoves = $derived(shown.length > 0 && shown.every((m) => checked[m.id]))
69
+
70
+ function bulk() {
71
+ const on = !bulkRemoves
72
+ for (const model of shown) checked[model.id] = on
73
+ emit()
74
+ }
75
+ function toggle(id: string) {
76
+ checked[id] = !checked[id]
77
+ emit()
78
+ }
79
+ function setType(id: string, type: ModelType) {
80
+ types[id] = type
81
+ if (checked[id]) emit()
82
+ }
83
+
84
+ /** Models outside this probe (added by hand, or no longer listed) are kept untouched. */
85
+ function emit() {
86
+ onchange(mergeModels(selected, models, checked, types))
87
+ }
88
+ </script>
89
+
90
+ <Dialog.Root bind:open>
91
+ <Dialog.Content class="flex max-h-[85dvh] min-w-0 flex-col gap-3 sm:max-w-2xl">
92
+ <Dialog.Header class="shrink-0 text-left">
93
+ <Dialog.Title class="flex items-center gap-2 pr-6">
94
+ {t('providers.picker_title')}
95
+ <span aria-hidden="true" class="bg-muted rounded px-1.5 text-kit-xs tabular-nums"
96
+ >{models.length}</span
97
+ >
98
+ </Dialog.Title>
99
+ <Dialog.Description>{t('providers.picker_hint')}</Dialog.Description>
100
+ </Dialog.Header>
101
+ <div class="flex shrink-0 items-center gap-2">
102
+ <Input
103
+ class="min-w-0 flex-1"
104
+ bind:value={query}
105
+ placeholder={t('providers.search_models')}
106
+ aria-label={t('providers.search_models')}
107
+ autocomplete="off"
108
+ spellcheck={false}
109
+ />
110
+ <Button
111
+ variant="outline"
112
+ size="sm"
113
+ class="shrink-0"
114
+ disabled={shown.length === 0}
115
+ onclick={bulk}
116
+ >
117
+ {t(bulkRemoves ? 'providers.remove_all' : 'providers.add_all')}
118
+ </Button>
119
+ </div>
120
+ <div
121
+ class="flex shrink-0 gap-1 overflow-x-auto pb-1"
122
+ role="group"
123
+ aria-label={t('providers.model_type')}
124
+ >
125
+ {#each ['all', ...TYPES] as type (type)}
126
+ {@const hits =
127
+ type === 'all'
128
+ ? searched.length
129
+ : searched.filter((m) => (types[m.id] ?? m.modelType) === type).length}
130
+ <Button
131
+ variant={filter === type ? 'secondary' : 'ghost'}
132
+ size="sm"
133
+ class="shrink-0"
134
+ aria-pressed={filter === type}
135
+ disabled={hits === 0 && type !== 'all'}
136
+ onclick={() => (filter = type as ModelType | 'all')}
137
+ >
138
+ {type === 'all' ? t('providers.all_types') : t(`providers.type.${type as ModelType}`)}
139
+ <span class="text-muted-foreground tabular-nums">{hits}</span>
140
+ </Button>
141
+ {/each}
142
+ </div>
143
+ <div
144
+ class="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto"
145
+ role="region"
146
+ aria-label={t('providers.models')}
147
+ >
148
+ {#if shown.length === 0}
149
+ <p class="text-muted-foreground py-8 text-center text-kit-sm">
150
+ {t('providers.no_model_matches')}
151
+ </p>
152
+ {/if}
153
+ {#each groups as [group, items = []] (group)}
154
+ {@const folded = !query.trim() && collapsed[group]}
155
+ <section class="flex min-w-0 flex-col">
156
+ <Button
157
+ variant="ghost"
158
+ size="sm"
159
+ class="justify-start"
160
+ aria-expanded={!folded}
161
+ onclick={() => (collapsed[group] = !collapsed[group])}
162
+ >
163
+ <ChevronRight class={folded ? '' : 'rotate-90'} />
164
+ {group || t('providers.no_group')}
165
+ <span class="bg-muted rounded px-1.5 tabular-nums">{items.length}</span>
166
+ </Button>
167
+ {#if !folded}
168
+ <ul>
169
+ {#each items as model (model.id)}
170
+ <li
171
+ class="hover:bg-accent/50 flex min-w-0 items-center gap-1 rounded-md py-0.5 pl-4"
172
+ >
173
+ <span class="min-w-0 flex-1 truncate font-mono text-kit-sm" title={model.id}
174
+ >{model.id}</span
175
+ >
176
+ <Select.Root
177
+ type="single"
178
+ value={types[model.id] ?? model.modelType}
179
+ onValueChange={(value) => setType(model.id, value as ModelType)}
180
+ >
181
+ <Select.Trigger
182
+ size="sm"
183
+ class="shrink-0 border-0 bg-transparent shadow-none"
184
+ aria-label={t('providers.type_for', { id: model.id })}
185
+ >
186
+ {t(`providers.type.${types[model.id] ?? model.modelType}`)}
187
+ </Select.Trigger>
188
+ <Select.Content
189
+ ><Select.Group>
190
+ {#each TYPES as type (type)}
191
+ <Select.Item value={type} label={t(`providers.type.${type}`)} />
192
+ {/each}
193
+ </Select.Group></Select.Content
194
+ >
195
+ </Select.Root>
196
+ {#if model.canVision}<span
197
+ class="text-muted-foreground hidden shrink-0 text-kit-xs sm:inline"
198
+ >{t('providers.vision')}</span
199
+ >{/if}
200
+ <span class="text-success w-8 shrink-0 text-right text-kit-xs"
201
+ >{checked[model.id] ? t('providers.picked') : ''}</span
202
+ >
203
+ <Button
204
+ variant="ghost"
205
+ size="icon-sm"
206
+ class="shrink-0"
207
+ aria-label={t(
208
+ checked[model.id] ? 'providers.remove_model' : 'providers.add_model',
209
+ {
210
+ id: model.id,
211
+ },
212
+ )}
213
+ onclick={() => toggle(model.id)}
214
+ >
215
+ {#if checked[model.id]}<Minus />{:else}<Plus />{/if}
216
+ </Button>
217
+ </li>
218
+ {/each}
219
+ </ul>
220
+ {/if}
221
+ </section>
222
+ {/each}
223
+ </div>
224
+ <Dialog.Footer class="shrink-0">
225
+ <p class="text-muted-foreground mr-auto text-kit-xs" aria-live="polite">
226
+ {t('providers.picked_count', { count })}
227
+ </p>
228
+ </Dialog.Footer>
229
+ </Dialog.Content>
230
+ </Dialog.Root>
@@ -0,0 +1,125 @@
1
+ <script lang="ts">
2
+ import { getKitContext } from '../context'
3
+ import MinusIcon from '@lucide/svelte/icons/minus'
4
+ import PlusIcon from '@lucide/svelte/icons/plus'
5
+ import { Button } from '#lib/components/ui/button/index.js'
6
+ import * as Field from '#lib/components/ui/field/index.js'
7
+ import { Input } from '#lib/components/ui/input/index.js'
8
+ import type { Model, PricingTier } from '../ports'
9
+ import type { ModelField } from './draft'
10
+
11
+ let {
12
+ edit,
13
+ errorFor,
14
+ }: { edit: Model | null; errorFor: (field: ModelField) => { message: string }[] } = $props()
15
+ const { t } = getKitContext().i18n
16
+ const PRICES: (keyof PricingTier)[] = ['input', 'output', 'cacheRead', 'cacheWrite']
17
+ const CURRENCIES = [
18
+ ['USD', '$'],
19
+ ['CNY', '¥'],
20
+ ] as const
21
+ const CHIP = 'aria-pressed:border-primary aria-pressed:bg-primary/10 aria-pressed:text-primary'
22
+ const count = (value: number) => (Number.isNaN(value) ? null : value)
23
+ const tier = (minInputTokens: number): PricingTier => ({
24
+ minInputTokens,
25
+ input: 0,
26
+ output: 0,
27
+ cacheRead: null,
28
+ cacheWrite: null,
29
+ })
30
+ function setPrice(index: number, key: keyof PricingTier, value: number) {
31
+ const target = edit?.pricing?.tiers[index]
32
+ if (!target) return
33
+ if (key === 'cacheRead' || key === 'cacheWrite') target[key] = count(value)
34
+ else target[key] = value
35
+ }
36
+ function removeTier(index: number) {
37
+ const pricing = edit?.pricing
38
+ if (!edit || !pricing) return
39
+ pricing.tiers.splice(index, 1)
40
+ if (pricing.tiers[0]) pricing.tiers[0].minInputTokens = 0
41
+ else edit.pricing = null
42
+ }
43
+ </script>
44
+
45
+ <Field.Set>
46
+ <Field.Legend variant="label">{t('providers.pricing')}</Field.Legend>
47
+ {#if edit?.pricing}
48
+ {@const current = edit.pricing}
49
+ <div class="flex flex-wrap gap-1.5" role="group" aria-label={t('providers.currency')}>
50
+ {#each CURRENCIES as [currency, symbol] (currency)}
51
+ <Button
52
+ variant="outline"
53
+ size="sm"
54
+ class={CHIP}
55
+ aria-pressed={current.currency === currency}
56
+ aria-label={t(`providers.currency.${currency}`)}
57
+ onclick={() => (current.currency = currency)}>{symbol}</Button
58
+ >
59
+ {/each}
60
+ </div>
61
+ <p class="text-muted-foreground text-kit-xs">{t('providers.pricing_unit')}</p>
62
+ {#each current.tiers as item, index (index)}
63
+ <fieldset class="flex flex-col gap-2 rounded-kit border p-2">
64
+ <legend class="flex w-full items-center gap-2 px-1 text-kit-sm"
65
+ >{t('providers.tier', { n: index + 1 })}{#if current.tiers.length > 1}<Button
66
+ variant="ghost"
67
+ size="icon-xs"
68
+ class="ml-auto"
69
+ aria-label={t('providers.tier_remove', { n: index + 1 })}
70
+ title={t('providers.tier_remove', { n: index + 1 })}
71
+ onclick={() => removeTier(index)}><MinusIcon /></Button
72
+ >{/if}</legend
73
+ >
74
+ <div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
75
+ <label class="flex flex-col gap-1 text-kit-xs"
76
+ >{t('providers.tier_start')}<Input
77
+ type="number"
78
+ min="0"
79
+ step="1"
80
+ disabled={index === 0}
81
+ value={item.minInputTokens}
82
+ oninput={(event) =>
83
+ setPrice(index, 'minInputTokens', event.currentTarget.valueAsNumber)}
84
+ /></label
85
+ >
86
+ {#each PRICES as key (key)}
87
+ <label class="flex flex-col gap-1 text-kit-xs"
88
+ >{t(`providers.price.${key}`)}<Input
89
+ type="number"
90
+ min="0"
91
+ step="any"
92
+ placeholder={key === 'cacheRead' || key === 'cacheWrite'
93
+ ? t('providers.price_same_as_input')
94
+ : ''}
95
+ value={item[key] ?? ''}
96
+ oninput={(event) => setPrice(index, key, event.currentTarget.valueAsNumber)}
97
+ /></label
98
+ >
99
+ {/each}
100
+ </div>
101
+ </fieldset>
102
+ {/each}
103
+ <Field.Error errors={errorFor('pricing')} />
104
+ <div class="flex flex-wrap gap-1.5">
105
+ {#if current.tiers.length < 8}<Button
106
+ variant="outline"
107
+ size="sm"
108
+ onclick={() =>
109
+ current.tiers.push(tier((current.tiers.at(-1)?.minInputTokens ?? 0) + 200000))}
110
+ ><PlusIcon />{t('providers.tier_add')}</Button
111
+ >{/if}
112
+ <Button variant="ghost" size="sm" onclick={() => edit && (edit.pricing = null)}
113
+ >{t('providers.pricing_clear')}</Button
114
+ >
115
+ </div>
116
+ {:else}
117
+ <Button
118
+ variant="outline"
119
+ size="sm"
120
+ class="self-start"
121
+ onclick={() => edit && (edit.pricing = { currency: 'USD', tiers: [tier(0)] })}
122
+ ><PlusIcon />{t('providers.pricing_add')}</Button
123
+ >
124
+ {/if}
125
+ </Field.Set>