@ouraihub/aether-ui 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/package.json +3 -3
  2. package/src/chat/ApprovalCard.svelte +8 -2
  3. package/src/chat/ChatComposer.svelte +319 -0
  4. package/src/chat/ChatMessage.svelte +262 -0
  5. package/src/chat/ChatPanel.css +386 -0
  6. package/src/chat/ChatPanel.svelte +276 -1880
  7. package/src/chat/ChatPanelApprovals.svelte +28 -0
  8. package/src/chat/ChatPanelComposer.svelte +336 -0
  9. package/src/chat/ChatPanelDialogs.svelte +242 -0
  10. package/src/chat/ChatPanelHeader.svelte +108 -0
  11. package/src/chat/ChatPanelMessages.svelte +166 -0
  12. package/src/chat/ChatPanelMode.svelte +190 -0
  13. package/src/chat/ChatPanelState.svelte.ts +1 -0
  14. package/src/chat/ChatPanelToolbar.svelte +436 -0
  15. package/src/chat/ChatSurface.svelte +168 -0
  16. package/src/chat/ContextUsageStatus.svelte +97 -0
  17. package/src/chat/MessageItem.svelte +111 -214
  18. package/src/chat/ModelEffortPicker.svelte +326 -0
  19. package/src/chat/PickerEffort.svelte +71 -0
  20. package/src/chat/ReasoningControl.svelte +184 -0
  21. package/src/chat/ReasoningSlider.svelte +174 -0
  22. package/src/chat/SkillPicker.svelte +14 -6
  23. package/src/chat/UsageControl.svelte +223 -0
  24. package/src/chat/UsageDetails.svelte +44 -90
  25. package/src/chat/chat-attachments.svelte.ts +78 -0
  26. package/src/chat/chat-panel-history.svelte.ts +430 -0
  27. package/src/chat/chat-panel-models.svelte.ts +97 -0
  28. package/src/chat/chat-panel-props.ts +38 -0
  29. package/src/chat/context-status.ts +101 -0
  30. package/src/chat/icons/LICENSE-REMIX +186 -0
  31. package/src/chat/icons/LICENSE-SIMPLE-ICONS +30 -0
  32. package/src/chat/icons/PathIcon.svelte +8 -0
  33. package/src/chat/icons/effort-icons.ts +65 -0
  34. package/src/chat/icons/status-icons.ts +105 -0
  35. package/src/chat/index.ts +25 -1
  36. package/src/chat/model-effort.ts +177 -0
  37. package/src/chat/run-session-history.ts +56 -0
  38. package/src/chat/run-session-types.ts +69 -0
  39. package/src/chat/run-session.svelte.ts +73 -135
  40. package/src/chat/test-support/chat-fixture.ts +194 -0
  41. package/src/chat/test-support/conversation-fixtures.ts +32 -0
  42. package/src/chat/test-support/run-session-fixture.ts +112 -0
  43. package/src/dev/Demo.svelte +3 -1
  44. package/src/i18n/en-chat.d.ts +1 -0
  45. package/src/i18n/en-chat.js +201 -0
  46. package/src/i18n/en.ts +58 -180
  47. package/src/i18n/index.ts +7 -2
  48. package/src/i18n/zh-CN-chat.d.ts +1 -0
  49. package/src/i18n/zh-CN-chat.js +190 -0
  50. package/src/i18n/zh-CN.ts +8 -153
  51. package/src/logs/LogPanel.svelte +9 -3
  52. package/src/logs/LogSettings.svelte +3 -1
  53. package/src/mcp/McpClientEditor.svelte +204 -0
  54. package/src/mcp/McpClientSettings.svelte +57 -130
  55. package/src/mcp/McpServerSettings.svelte +47 -11
  56. package/src/mcp/input.ts +2 -1
  57. package/src/ports/contract.ts +11 -2
  58. package/src/ports/fake-optional.ts +81 -0
  59. package/src/ports/fake.ts +90 -86
  60. package/src/ports/search.ts +11 -2
  61. package/src/ports/test-support/fake-fixture.ts +63 -0
  62. package/src/providers/EndpointKeysDialog.svelte +27 -13
  63. package/src/providers/EndpointModels.svelte +311 -0
  64. package/src/providers/EndpointPanel.svelte +33 -258
  65. package/src/providers/ModelPickerDialog.svelte +59 -16
  66. package/src/providers/ModelPricing.svelte +125 -0
  67. package/src/providers/ModelSelect.svelte +22 -8
  68. package/src/providers/ModelSettingsDialog.svelte +61 -126
  69. package/src/providers/ProviderDialogs.svelte +120 -0
  70. package/src/providers/ProviderList.svelte +159 -0
  71. package/src/providers/ProvidersSettings.svelte +67 -207
  72. package/src/providers/RequestConfigDialog.svelte +40 -12
  73. package/src/providers/SecretInput.svelte +11 -3
  74. package/src/providers/draft.ts +39 -13
  75. package/src/providers/edit-session.ts +31 -7
  76. package/src/providers/reasoning.ts +18 -4
  77. package/src/providers/revision.svelte.ts +3 -1
  78. package/src/providers/test-support/providers-fixture.ts +120 -0
  79. package/src/search/SearchAdvancedTools.svelte +182 -0
  80. package/src/search/SearchProviderCard.svelte +195 -0
  81. package/src/search/SearchSettings.svelte +62 -265
  82. package/src/search/settings-flow.ts +8 -3
  83. package/src/skills/SkillAdd.svelte +51 -18
  84. package/src/skills/SkillAuthoring.svelte +69 -20
  85. package/src/skills/SkillEditor.svelte +13 -3
  86. package/src/skills/SkillPreviewDialog.svelte +19 -5
  87. package/src/skills/SkillsSettings.svelte +11 -4
  88. package/src/skills/test-support/skills-fixture.ts +139 -0
  89. package/src/tauri/index.ts +23 -7
  90. package/src/web/index.ts +90 -25
@@ -0,0 +1,311 @@
1
+ <script lang="ts">
2
+ import BrainIcon from '@lucide/svelte/icons/brain'
3
+ import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
4
+ import ChevronsDownUpIcon from '@lucide/svelte/icons/chevrons-down-up'
5
+ import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down'
6
+ import EyeIcon from '@lucide/svelte/icons/eye'
7
+ import Globe2Icon from '@lucide/svelte/icons/globe-2'
8
+ import MinusIcon from '@lucide/svelte/icons/minus'
9
+ import PlusIcon from '@lucide/svelte/icons/plus'
10
+ import RefreshIcon from '@lucide/svelte/icons/refresh-cw'
11
+ import SearchIcon from '@lucide/svelte/icons/search'
12
+ import Settings2Icon from '@lucide/svelte/icons/settings-2'
13
+ import WrenchIcon from '@lucide/svelte/icons/wrench'
14
+ import { Button } from '#lib/components/ui/button/index.js'
15
+ import * as Dialog from '#lib/components/ui/dialog/index.js'
16
+ import * as Field from '#lib/components/ui/field/index.js'
17
+ import { Input } from '#lib/components/ui/input/index.js'
18
+ import * as Select from '#lib/components/ui/select/index.js'
19
+ import { getKitContext } from '../context'
20
+ import type { Model } from '../ports'
21
+ import { cn } from '../utils'
22
+ import { groupKey, type EndpointDraft } from './draft'
23
+ import ModelSettingsDialog from './ModelSettingsDialog.svelte'
24
+
25
+ let {
26
+ endpoint,
27
+ onsync,
28
+ oncommit,
29
+ usedBy,
30
+ targetModel,
31
+ }: {
32
+ endpoint: EndpointDraft
33
+ onsync: () => void
34
+ oncommit: () => void
35
+ usedBy?: ((endpointId: string, modelId: string) => string[]) | undefined
36
+ targetModel?: Model | null
37
+ } = $props()
38
+
39
+ const { i18n } = getKitContext()
40
+ const { t } = i18n
41
+ const CAPS = ['vision', 'reasoning', 'tools', 'web'] as const
42
+ type Cap = (typeof CAPS)[number]
43
+ const CAP_STYLE: Record<Cap, string> = {
44
+ vision: 'bg-success/15 text-success',
45
+ reasoning: 'bg-info/15 text-info',
46
+ tools: 'bg-warning/15 text-warning',
47
+ web: 'bg-primary/10 text-primary',
48
+ }
49
+ let modelOpen = $state(false)
50
+ let editing = $state<Model | null>(null)
51
+ let openedTarget = $state<string | null>(null)
52
+ $effect.pre(() => {
53
+ if (targetModel && openedTarget !== targetModel.id) {
54
+ openedTarget = targetModel.id
55
+ editing = targetModel
56
+ modelOpen = true
57
+ }
58
+ })
59
+ let addOpen = $state(false)
60
+ let newModelId = $state('')
61
+ let searching = $state(false)
62
+ let query = $state('')
63
+ let capFilter = $state<Cap | 'all'>('all')
64
+ let folded = $state<Record<string, boolean>>({})
65
+
66
+ const has = (model: Model, cap: Cap) =>
67
+ cap === 'vision'
68
+ ? model.canVision
69
+ : cap === 'reasoning'
70
+ ? model.reasoning !== null
71
+ : cap === 'tools'
72
+ ? model.supportsTools === true
73
+ : model.nativeWebSearch
74
+ const shown = $derived.by(() => {
75
+ const search = query.trim().toLowerCase()
76
+ return endpoint.models.filter(
77
+ (model) =>
78
+ (model.id.toLowerCase().includes(search) || !!model.name?.toLowerCase().includes(search)) &&
79
+ (capFilter === 'all' || has(model, capFilter)),
80
+ )
81
+ })
82
+ const groups = $derived(Object.entries(Object.groupBy(shown, groupKey)))
83
+ const allFolded = $derived(groups.length > 0 && groups.every(([group]) => folded[group]))
84
+
85
+ function setModels(models: Model[]) {
86
+ endpoint.models = models
87
+ oncommit()
88
+ }
89
+ function addModel() {
90
+ const id = newModelId.trim()
91
+ if (id && !endpoint.models.some((model) => model.id === id)) {
92
+ setModels([
93
+ ...endpoint.models,
94
+ {
95
+ id,
96
+ name: null,
97
+ groupName: null,
98
+ modelType: 'chat',
99
+ typeSource: 'user',
100
+ contextWindow: null,
101
+ maxOutput: null,
102
+ canVision: false,
103
+ supportsTools: null,
104
+ reasoning: null,
105
+ nativeWebSearch: false,
106
+ pricing: null,
107
+ },
108
+ ])
109
+ }
110
+ newModelId = ''
111
+ addOpen = false
112
+ }
113
+ </script>
114
+
115
+ <section class="flex min-w-0 flex-col gap-2" aria-labelledby="kit-endpoint-models">
116
+ <div class="flex min-w-0 flex-wrap items-center gap-1">
117
+ <h3 id="kit-endpoint-models" class="mr-1 text-kit-base font-semibold">
118
+ {t('providers.models')}
119
+ </h3>
120
+ <Button
121
+ variant="ghost"
122
+ size="icon-xs"
123
+ title={t(allFolded ? 'providers.expand_all' : 'providers.collapse_all')}
124
+ aria-label={t(allFolded ? 'providers.expand_all' : 'providers.collapse_all')}
125
+ disabled={groups.length === 0}
126
+ onclick={() =>
127
+ (folded = allFolded ? {} : Object.fromEntries(groups.map(([group]) => [group, true])))}
128
+ >{#if allFolded}<ChevronsUpDownIcon />{:else}<ChevronsDownUpIcon />{/if}</Button
129
+ >
130
+ {#if searching || query}
131
+ <Input
132
+ class="h-7 w-36 text-kit-sm"
133
+ autofocus
134
+ placeholder={t('providers.model_search')}
135
+ aria-label={t('providers.model_search')}
136
+ bind:value={query}
137
+ onblur={() => (searching = false)}
138
+ onkeydown={(event) => event.key === 'Escape' && ((query = ''), event.currentTarget.blur())}
139
+ />
140
+ {:else}
141
+ <Button
142
+ variant="ghost"
143
+ size="icon-xs"
144
+ title={t('providers.model_search')}
145
+ aria-label={t('providers.model_search')}
146
+ onclick={() => (searching = true)}><SearchIcon /></Button
147
+ >
148
+ {/if}
149
+ <Select.Root
150
+ type="single"
151
+ value={capFilter}
152
+ onValueChange={(value) => (capFilter = value as Cap | 'all')}
153
+ >
154
+ <Select.Trigger
155
+ size="sm"
156
+ class={cn('border-0 shadow-none', capFilter !== 'all' && 'bg-accent')}
157
+ aria-label={t('providers.cap_filter')}
158
+ >
159
+ {capFilter === 'all' ? t('providers.all_types') : t(`providers.cap_${capFilter}`)}
160
+ </Select.Trigger>
161
+ <Select.Content>
162
+ <Select.Group>
163
+ <Select.Item value="all" label={t('providers.all_types')} />
164
+ {#each CAPS as cap (cap)}<Select.Item
165
+ value={cap}
166
+ label={t(`providers.cap_${cap}`)}
167
+ />{/each}
168
+ </Select.Group>
169
+ </Select.Content>
170
+ </Select.Root>
171
+ <div class="ml-auto flex items-center gap-1">
172
+ <Button variant="outline" size="sm" disabled={!endpoint.baseUrl} onclick={onsync}>
173
+ <RefreshIcon />{t('providers.sync_models')}
174
+ </Button>
175
+ <Button
176
+ variant="outline"
177
+ size="icon-sm"
178
+ title={t('providers.add_model_manual')}
179
+ aria-label={t('providers.add_model_manual')}
180
+ onclick={() => (addOpen = true)}><PlusIcon /></Button
181
+ >
182
+ </div>
183
+ </div>
184
+
185
+ {#each groups as [group, models = []] (group)}
186
+ <section class="group/model-group overflow-hidden rounded-kit border">
187
+ <div class="bg-muted/30 flex items-center gap-1 pr-2">
188
+ <button
189
+ type="button"
190
+ class="flex min-w-0 flex-1 items-center gap-2 px-3 py-2 text-left text-kit-sm"
191
+ aria-expanded={!folded[group]}
192
+ onclick={() => (folded[group] = !folded[group])}
193
+ >
194
+ <ChevronRightIcon
195
+ class={cn('size-4 shrink-0 transition-transform', !folded[group] && 'rotate-90')}
196
+ />
197
+ <span class="truncate">{group || t('providers.no_group')}</span>
198
+ <span class="text-muted-foreground text-kit-xs tabular-nums">{models.length}</span>
199
+ </button>
200
+ <Button
201
+ variant="ghost"
202
+ size="icon-xs"
203
+ class="opacity-0 group-hover/model-group:opacity-100 focus-visible:opacity-100"
204
+ title={t('providers.remove_group')}
205
+ aria-label={t('providers.remove_group')}
206
+ onclick={() => setModels(endpoint.models.filter((model) => !models.includes(model)))}
207
+ ><MinusIcon /></Button
208
+ >
209
+ </div>
210
+ {#if !folded[group]}
211
+ <ul class="divide-y border-t">
212
+ {#each models as model (model.id)}
213
+ <li class="hover:bg-accent/40 flex min-w-0 items-center gap-2 px-3 py-1.5">
214
+ <span
215
+ aria-hidden="true"
216
+ class="bg-muted text-muted-foreground flex size-6 shrink-0 items-center justify-center rounded-full text-kit-xs uppercase"
217
+ >{(model.name ?? model.id).slice(0, 1)}</span
218
+ >
219
+ <span class="min-w-0 flex-1 truncate text-kit-sm" title={model.id}
220
+ >{model.name ?? model.id}</span
221
+ >
222
+ <span class="flex shrink-0 items-center gap-0.5">
223
+ {#each CAPS.filter((cap) => has(model, cap)) as cap (cap)}
224
+ <span
225
+ class={cn(
226
+ 'flex h-5 w-6 items-center justify-center rounded-full',
227
+ CAP_STYLE[cap],
228
+ )}
229
+ title={t(`providers.cap_${cap}`)}
230
+ >
231
+ {#if cap === 'vision'}<EyeIcon
232
+ class="size-3"
233
+ />{:else if cap === 'reasoning'}<BrainIcon
234
+ class="size-3"
235
+ />{:else if cap === 'tools'}<WrenchIcon class="size-3" />{:else}<Globe2Icon
236
+ class="size-3"
237
+ />{/if}
238
+ <span class="sr-only">{t(`providers.cap_${cap}`)}</span>
239
+ </span>
240
+ {/each}
241
+ </span>
242
+ <Button
243
+ variant="ghost"
244
+ size="icon-xs"
245
+ title={t('providers.model_settings')}
246
+ aria-label={t('providers.model_settings_for', { id: model.id })}
247
+ onclick={() => {
248
+ editing = model
249
+ modelOpen = true
250
+ }}><Settings2Icon /></Button
251
+ >
252
+ <Button
253
+ variant="ghost"
254
+ size="icon-xs"
255
+ title={t('providers.remove_model', { id: model.id })}
256
+ aria-label={t('providers.remove_model', { id: model.id })}
257
+ onclick={() => setModels(endpoint.models.filter((item) => item.id !== model.id))}
258
+ ><MinusIcon /></Button
259
+ >
260
+ </li>
261
+ {/each}
262
+ </ul>
263
+ {/if}
264
+ </section>
265
+ {:else}
266
+ <p
267
+ class="text-muted-foreground rounded-kit border border-dashed px-3 py-6 text-center text-kit-sm"
268
+ >
269
+ {endpoint.models.length === 0 ? t('providers.no_models') : t('providers.no_model_matches')}
270
+ </p>
271
+ {/each}
272
+ </section>
273
+
274
+ <ModelSettingsDialog
275
+ bind:open={modelOpen}
276
+ model={editing}
277
+ protocol={endpoint.protocol}
278
+ usedBy={editing && endpoint.saved && usedBy ? usedBy(endpoint.saved.id, editing.id) : []}
279
+ onsave={(saved) =>
280
+ setModels(endpoint.models.map((model) => (model.id === saved.id ? saved : model)))}
281
+ />
282
+
283
+ <Dialog.Root bind:open={addOpen}>
284
+ <Dialog.Content class="sm:max-w-md">
285
+ <Dialog.Header><Dialog.Title>{t('providers.add_model_manual')}</Dialog.Title></Dialog.Header>
286
+ <form
287
+ class="contents"
288
+ onsubmit={(event) => {
289
+ event.preventDefault()
290
+ addModel()
291
+ }}
292
+ >
293
+ <Field.Field>
294
+ <Field.Label for="kit-model-new">{t('providers.model_id')}</Field.Label>
295
+ <Input
296
+ id="kit-model-new"
297
+ class="font-mono"
298
+ autocomplete="off"
299
+ spellcheck={false}
300
+ bind:value={newModelId}
301
+ />
302
+ </Field.Field>
303
+ <Dialog.Footer>
304
+ <Button type="button" variant="outline" onclick={() => (addOpen = false)}
305
+ >{t('common.cancel')}</Button
306
+ >
307
+ <Button type="submit" disabled={!newModelId.trim()}>{t('common.confirm')}</Button>
308
+ </Dialog.Footer>
309
+ </form>
310
+ </Dialog.Content>
311
+ </Dialog.Root>