@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,120 @@
1
+ import { render } from '@testing-library/svelte'
2
+ import { vi } from 'vitest'
3
+ import { createI18n } from '../../i18n'
4
+ import {
5
+ createFakePorts,
6
+ type Endpoint,
7
+ type Provider,
8
+ type ProviderPreset,
9
+ type ProviderSave,
10
+ } from '../../ports'
11
+ import Harness from '../Harness.test.svelte'
12
+
13
+ export const endpoint: Endpoint = {
14
+ id: 'e1',
15
+ slug: 'main',
16
+ baseUrl: 'http://127.0.0.1:9/v1',
17
+ protocol: 'openai-chat',
18
+ keys: [
19
+ {
20
+ id: 'k1',
21
+ label: '',
22
+ enabled: true,
23
+ masked: 'sk-a****wxyz',
24
+ readable: true,
25
+ exhaustedUntil: null,
26
+ exhaustedReason: null,
27
+ },
28
+ ],
29
+ keyStrategy: 'round_robin',
30
+ probeUrl: 'http://127.0.0.1:9/v1/models',
31
+ imageBaseUrl: null,
32
+ imageEditBaseUrl: null,
33
+ headers: { 'X-Team': 'a' },
34
+ models: [],
35
+ }
36
+
37
+ export const stored: Provider = {
38
+ id: 'p1',
39
+ name: 'Local',
40
+ enabled: true,
41
+ notes: '',
42
+ presetId: null,
43
+ endpoints: [endpoint],
44
+ }
45
+
46
+ export const twoKeys: Endpoint = {
47
+ ...endpoint,
48
+ keys: [
49
+ ...endpoint.keys,
50
+ {
51
+ id: 'k2',
52
+ label: 'backup',
53
+ enabled: true,
54
+ masked: 'sk-b****1234',
55
+ readable: true,
56
+ exhaustedUntil: null,
57
+ exhaustedReason: null,
58
+ },
59
+ ],
60
+ }
61
+
62
+ export const model = {
63
+ name: null,
64
+ groupName: null,
65
+ pricing: null,
66
+ contextWindow: null,
67
+ maxOutput: null,
68
+ canVision: false,
69
+ supportsTools: null,
70
+ nativeWebSearch: false,
71
+ } as const
72
+
73
+ export function setup(
74
+ overrides: NonNullable<Parameters<typeof createFakePorts>[0]>['providers'] = {},
75
+ list = stored,
76
+ presets?: ProviderPreset[],
77
+ ) {
78
+ const saves: ProviderSave[] = []
79
+ const ports = createFakePorts({
80
+ providers: {
81
+ list: () => Promise.resolve([list]),
82
+ save: (input) => {
83
+ saves.push(input)
84
+ return Promise.resolve([stored])
85
+ },
86
+ probe: () =>
87
+ Promise.resolve({
88
+ probeUrl: endpoint.probeUrl,
89
+ error: null,
90
+ suggestedProtocol: null,
91
+ models: [
92
+ {
93
+ ...model,
94
+ id: 'gpt-4o',
95
+ group: 'gpt',
96
+ modelType: 'chat',
97
+ typeSource: 'inferred',
98
+ reasoning: null,
99
+ },
100
+ {
101
+ ...model,
102
+ id: 'text-embedding-3',
103
+ group: null,
104
+ modelType: 'embedding',
105
+ typeSource: 'inferred',
106
+ reasoning: null,
107
+ },
108
+ ],
109
+ }),
110
+ ...overrides,
111
+ },
112
+ })
113
+ const notify = { toast: vi.fn(), confirm: () => Promise.resolve(true) }
114
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }
115
+ render(Harness, {
116
+ kit: { ports, i18n: createI18n('en'), notify, logger },
117
+ ...(presets ? { presets } : {}),
118
+ })
119
+ return { saves, notify }
120
+ }
@@ -0,0 +1,182 @@
1
+ <script lang="ts">
2
+ import { Button } from '#lib/components/ui/button/index.js'
3
+ import { Input } from '#lib/components/ui/input/index.js'
4
+ import { Textarea } from '#lib/components/ui/textarea/index.js'
5
+ import * as Field from '#lib/components/ui/field/index.js'
6
+ import { getKitContext } from '../context'
7
+ import type { SearchPage, SearchPreferences, SearchResult } from '../ports'
8
+ let {
9
+ preferences = $bindable(),
10
+ blacklist = $bindable(),
11
+ query = $bindable(),
12
+ pageUrl = $bindable(),
13
+ preferencesPending,
14
+ preferencesError,
15
+ result,
16
+ page,
17
+ queryPending,
18
+ pagePending,
19
+ queryError,
20
+ pageError,
21
+ onSavePreferences,
22
+ onSearch,
23
+ onReadPage,
24
+ }: {
25
+ preferences: SearchPreferences
26
+ blacklist: string
27
+ query: string
28
+ pageUrl: string
29
+ preferencesPending: boolean
30
+ preferencesError: string
31
+ result: SearchResult | null
32
+ page: SearchPage | null
33
+ queryPending: boolean
34
+ pagePending: boolean
35
+ queryError: string
36
+ pageError: string
37
+ onSavePreferences: () => void
38
+ onSearch: () => void
39
+ onReadPage: () => void
40
+ } = $props()
41
+
42
+ const { i18n } = getKitContext()
43
+ const { t } = i18n
44
+ const safeUrl = (raw?: string) => {
45
+ if (!raw) return null
46
+ try {
47
+ const url = new URL(raw)
48
+ return ['https:', 'http:'].includes(url.protocol) ? url.href : null
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+ </script>
54
+
55
+ <section class="rounded-kit border p-4">
56
+ <details>
57
+ <summary class="cursor-pointer text-kit-base font-semibold">{t('search.advanced')}</summary>
58
+ <Field.Group class="mt-3 gap-3">
59
+ <Field.Field orientation="horizontal"
60
+ ><Field.Label for="search-max-results">{t('search.maxResults')}</Field.Label><Input
61
+ id="search-max-results"
62
+ class="ml-auto w-20"
63
+ type="number"
64
+ min="1"
65
+ max="50"
66
+ disabled={preferencesPending}
67
+ value={preferences.maxResults}
68
+ oninput={(event) => (preferences.maxResults = +event.currentTarget.value)}
69
+ /></Field.Field
70
+ >
71
+ <Field.Field orientation="horizontal"
72
+ ><Field.Label for="search-compression">{t('search.compression')}</Field.Label><select
73
+ id="search-compression"
74
+ class="bg-background border-input h-kit-control ml-auto rounded-kit-sm border px-2 text-kit-sm"
75
+ disabled={preferencesPending}
76
+ value={preferences.compression}
77
+ onchange={(event) =>
78
+ (preferences.compression = event.currentTarget
79
+ .value as SearchPreferences['compression'])}
80
+ ><option value="none">{t('search.compressionNone')}</option><option value="cutoff"
81
+ >{t('search.compressionCutoff')}</option
82
+ ></select
83
+ ></Field.Field
84
+ >
85
+ {#if preferences.compression === 'cutoff'}<Field.Field orientation="horizontal"
86
+ ><Field.Label for="search-cutoff">{t('search.cutoffLimit')}</Field.Label><Input
87
+ id="search-cutoff"
88
+ class="ml-auto w-24"
89
+ type="number"
90
+ min="100"
91
+ max="20000"
92
+ disabled={preferencesPending}
93
+ value={preferences.cutoffLimit}
94
+ oninput={(event) => (preferences.cutoffLimit = +event.currentTarget.value)}
95
+ /></Field.Field
96
+ >{/if}
97
+ <Field.Field
98
+ ><Field.Label for="search-blacklist">{t('search.blacklist')}</Field.Label><Textarea
99
+ id="search-blacklist"
100
+ rows={3}
101
+ disabled={preferencesPending}
102
+ placeholder="example.com\n*://*.example.org/*"
103
+ value={blacklist}
104
+ oninput={(event) => (blacklist = event.currentTarget.value)}
105
+ /></Field.Field
106
+ >
107
+ </Field.Group>
108
+ </details>
109
+ <Field.Field class="mt-3 border-t pt-3"
110
+ ><label class="flex items-center gap-2 text-kit-sm"
111
+ ><input
112
+ type="checkbox"
113
+ disabled={preferencesPending}
114
+ checked={preferences.modelToolsPreferred}
115
+ onchange={(event) => (preferences.modelToolsPreferred = event.currentTarget.checked)}
116
+ />{t('search.nativePreferred')}</label
117
+ ><Field.Description>{t('search.nativePrivacy')}</Field.Description></Field.Field
118
+ >
119
+ {#if preferencesError}<p role="alert" class="text-destructive text-kit-sm">
120
+ {preferencesError}
121
+ </p>{/if}
122
+ <Button
123
+ class="mt-3"
124
+ type="button"
125
+ size="sm"
126
+ disabled={preferencesPending}
127
+ onclick={onSavePreferences}>{t('search.save')}</Button
128
+ >
129
+ </section>
130
+ <section class="flex min-w-0 flex-col gap-3 rounded-kit border p-4" aria-label={t('search.tools')}>
131
+ <Field.Field
132
+ ><Field.Label for="search-query">{t('search.query')}</Field.Label>
133
+ <div class="flex min-w-0 gap-2">
134
+ <Input
135
+ id="search-query"
136
+ class="min-w-0 flex-1"
137
+ disabled={queryPending}
138
+ value={query}
139
+ oninput={(event) => (query = event.currentTarget.value)}
140
+ /><Button type="button" disabled={queryPending || !query.trim()} onclick={onSearch}
141
+ >{t('search.runSearch')}</Button
142
+ >
143
+ </div></Field.Field
144
+ >
145
+ {#if queryError}<p class="text-destructive text-kit-sm" role="alert">{queryError}</p>{/if}
146
+ {#if result}<p class="text-muted-foreground text-kit-sm">
147
+ {t('search.resultCount', { count: result.hits.length })}
148
+ </p>
149
+ {#each result.hits as hit (hit.url)}<article class="min-w-0 text-kit-sm">
150
+ <a
151
+ class="text-primary break-words hover:underline"
152
+ href={safeUrl(hit.url) ?? ''}
153
+ target="_blank"
154
+ rel="noopener noreferrer">{hit.title}</a
155
+ >
156
+ <p class="whitespace-pre-wrap break-words">{hit.snippet}</p>
157
+ </article>{/each}{/if}
158
+ <Field.Field
159
+ ><Field.Label for="search-page-url">{t('search.pageUrl')}</Field.Label>
160
+ <div class="flex min-w-0 gap-2">
161
+ <Input
162
+ id="search-page-url"
163
+ class="min-w-0 flex-1"
164
+ disabled={pagePending}
165
+ value={pageUrl}
166
+ oninput={(event) => (pageUrl = event.currentTarget.value)}
167
+ /><Button type="button" disabled={pagePending || !pageUrl.trim()} onclick={onReadPage}
168
+ >{t('search.read')}</Button
169
+ >
170
+ </div></Field.Field
171
+ >
172
+ {#if pageError}<p class="text-destructive text-kit-sm" role="alert">{pageError}</p>{/if}
173
+ {#if page}<h3 class="text-kit-base font-medium">{page.title}</h3>
174
+ <a
175
+ class="text-primary break-words text-kit-sm"
176
+ href={safeUrl(page.url) ?? ''}
177
+ target="_blank"
178
+ rel="noopener noreferrer">{page.url}</a
179
+ >
180
+ <pre
181
+ class="max-h-96 overflow-auto whitespace-pre-wrap break-words text-kit-sm">{page.content}</pre>{/if}
182
+ </section>
@@ -0,0 +1,195 @@
1
+ <script lang="ts">
2
+ import ActivityIcon from '@lucide/svelte/icons/activity'
3
+ import ListIcon from '@lucide/svelte/icons/list'
4
+ import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
5
+ import ExternalLinkIcon from '@lucide/svelte/icons/external-link'
6
+ import { Button } from '#lib/components/ui/button/index.js'
7
+ import { Input } from '#lib/components/ui/input/index.js'
8
+ import CopyInput from '../CopyInput.svelte'
9
+ import * as Alert from '#lib/components/ui/alert/index.js'
10
+ import * as Field from '#lib/components/ui/field/index.js'
11
+ import { getKitContext } from '../context'
12
+ import type { SearchCap, SearchProvider, SearchSettingsDto } from '../ports'
13
+
14
+ type Draft = { baseUrl?: string; basicAuthUsername?: string }
15
+ let {
16
+ cap,
17
+ provider,
18
+ settings,
19
+ pending,
20
+ error,
21
+ draft,
22
+ keyDraft,
23
+ check,
24
+ onChoose,
25
+ onOpenKeys,
26
+ onKeyInput,
27
+ onPatchDraft,
28
+ onSave,
29
+ }: {
30
+ cap: SearchCap
31
+ provider: SearchProvider
32
+ settings: SearchSettingsDto
33
+ pending: boolean
34
+ error: string
35
+ draft: Draft
36
+ keyDraft: string
37
+ check?: number | undefined
38
+ onChoose: (provider: string) => void
39
+ onOpenKeys: () => void
40
+ onKeyInput: (value: string) => void
41
+ onPatchDraft: (patch: Draft) => void
42
+ onSave: (check?: boolean) => void
43
+ } = $props()
44
+
45
+ const { i18n } = getKitContext()
46
+ const { t } = i18n
47
+ const safeUrl = (raw?: string) => {
48
+ if (!raw) return null
49
+ try {
50
+ const url = new URL(raw)
51
+ return ['https:', 'http:'].includes(url.protocol) ? url.href : null
52
+ } catch {
53
+ return null
54
+ }
55
+ }
56
+ </script>
57
+
58
+ <section
59
+ class="flex min-w-0 flex-col gap-3 rounded-kit border p-4"
60
+ aria-labelledby={`search-${cap}`}
61
+ >
62
+ <header class="flex flex-wrap items-start justify-between gap-3">
63
+ <div class="min-w-0 flex-1">
64
+ <h2 id={`search-${cap}`} class="text-kit-base font-semibold">{t(`search.${cap}`)}</h2>
65
+ <p class="text-muted-foreground text-kit-sm">
66
+ {provider.description}
67
+ {#if safeUrl(provider.officialWebsite)}<a
68
+ class="text-primary inline-flex"
69
+ href={safeUrl(provider.officialWebsite) ?? ''}
70
+ target="_blank"
71
+ rel="noopener noreferrer"
72
+ aria-label={t('search.website')}
73
+ title={t('search.website')}><ExternalLinkIcon size={13} /></a
74
+ >{/if}
75
+ </p>
76
+ </div>
77
+ <select
78
+ class="bg-background border-input h-kit-control max-w-full rounded-kit-sm border px-2 text-kit-sm"
79
+ aria-label={t(`search.${cap}`)}
80
+ disabled={pending}
81
+ value={settings.defaultProvider}
82
+ onchange={(event) => onChoose(event.currentTarget.value)}
83
+ >
84
+ {#each settings.providers as option (option.id)}<option value={option.id}
85
+ >{option.name}</option
86
+ >{/each}
87
+ </select>
88
+ </header>
89
+ {#if error}<Alert.Root variant="destructive"
90
+ ><Alert.Description>{error}</Alert.Description></Alert.Root
91
+ >{/if}
92
+ <Field.Group class="gap-3 border-t pt-3">
93
+ {#if provider.keyMode !== 'none' || provider.basicAuth}
94
+ <Field.Field data-disabled={pending}>
95
+ <div class="flex flex-wrap items-baseline gap-2">
96
+ <Field.Label for={`search-key-${cap}`}
97
+ >{provider.basicAuth ? t('search.password') : t('search.apiKey')}</Field.Label
98
+ >
99
+ {#if safeUrl(provider.apiKeyWebsite)}<a
100
+ class="text-primary text-kit-sm hover:underline"
101
+ href={safeUrl(provider.apiKeyWebsite) ?? ''}
102
+ target="_blank"
103
+ rel="noopener noreferrer">{t('search.apiKeyWebsite')}</a
104
+ >{/if}
105
+ </div>
106
+ <div class="flex min-w-0 items-center gap-2">
107
+ <Input
108
+ id={`search-key-${cap}`}
109
+ class="min-w-0 flex-1"
110
+ type="password"
111
+ autocomplete="off"
112
+ spellcheck={false}
113
+ maxlength={4096}
114
+ disabled={pending}
115
+ placeholder={t('search.newKey')}
116
+ value={keyDraft}
117
+ oninput={(event) => onKeyInput(event.currentTarget.value)}
118
+ />
119
+ <Button
120
+ type="button"
121
+ variant="outline"
122
+ size="icon-sm"
123
+ disabled={pending}
124
+ aria-label={t('search.manageKeys')}
125
+ title={t('search.manageKeys')}
126
+ onclick={onOpenKeys}><ListIcon /></Button
127
+ >
128
+ <Button
129
+ type="button"
130
+ variant="outline"
131
+ size="icon-sm"
132
+ disabled={pending}
133
+ aria-label={t('search.check')}
134
+ title={t('search.check')}
135
+ onclick={() => onSave(true)}
136
+ >
137
+ {#if pending}<LoaderCircleIcon class="animate-spin" />{:else}<ActivityIcon />{/if}
138
+ </Button>
139
+ </div>
140
+ <Field.Description
141
+ >{provider.keys.length
142
+ ? provider.keys.map((item) => item.masked).join(' · ')
143
+ : t('search.keyNotSet')}</Field.Description
144
+ >
145
+ </Field.Field>
146
+ {/if}
147
+ {#if provider.defaultBaseUrl !== undefined}
148
+ <Field.Field data-disabled={pending}>
149
+ <Field.Label for={`search-url-${cap}`}>{t('search.baseUrl')}</Field.Label>
150
+ <div class="flex min-w-0 items-center gap-2">
151
+ <CopyInput
152
+ id={`search-url-${cap}`}
153
+ class="min-w-0 flex-1"
154
+ disabled={pending}
155
+ value={draft.baseUrl ?? provider.baseUrl ?? provider.defaultBaseUrl}
156
+ oninput={(event) => onPatchDraft({ baseUrl: event.currentTarget.value })}
157
+ />
158
+ {#if provider.keyMode === 'none' && !provider.basicAuth}<Button
159
+ type="button"
160
+ variant="outline"
161
+ size="icon-sm"
162
+ disabled={pending}
163
+ aria-label={t('search.check')}
164
+ title={t('search.check')}
165
+ onclick={() => onSave(true)}><ActivityIcon /></Button
166
+ >{/if}
167
+ </div>
168
+ </Field.Field>
169
+ {/if}
170
+ {#if provider.basicAuth}
171
+ <Field.Field
172
+ ><Field.Label for={`search-user-${cap}`}>{t('search.username')}</Field.Label><Input
173
+ id={`search-user-${cap}`}
174
+ disabled={pending}
175
+ value={draft.basicAuthUsername ?? provider.basicAuthUsername ?? ''}
176
+ oninput={(event) => onPatchDraft({ basicAuthUsername: event.currentTarget.value })}
177
+ /></Field.Field
178
+ >
179
+ {/if}
180
+ </Field.Group>
181
+ <div class="flex flex-wrap items-center gap-2">
182
+ {#if provider.defaultBaseUrl !== undefined || provider.keyMode !== 'none'}<Button
183
+ type="button"
184
+ size="sm"
185
+ disabled={pending}
186
+ onclick={() => onSave()}>{t('search.save')}</Button
187
+ >{/if}
188
+ {#if pending}<span class="text-muted-foreground text-kit-sm" role="status"
189
+ >{t('search.checking')}</span
190
+ >
191
+ {:else if check !== undefined}<span class="text-muted-foreground text-kit-sm" role="status"
192
+ >{t('search.resultCount', { count: check })}</span
193
+ >{/if}
194
+ </div>
195
+ </section>