@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.
- package/package.json +3 -3
- package/src/chat/ApprovalCard.svelte +8 -2
- package/src/chat/ChatComposer.svelte +319 -0
- package/src/chat/ChatMessage.svelte +262 -0
- package/src/chat/ChatPanel.css +386 -0
- package/src/chat/ChatPanel.svelte +276 -1880
- package/src/chat/ChatPanelApprovals.svelte +28 -0
- package/src/chat/ChatPanelComposer.svelte +336 -0
- package/src/chat/ChatPanelDialogs.svelte +242 -0
- package/src/chat/ChatPanelHeader.svelte +108 -0
- package/src/chat/ChatPanelMessages.svelte +166 -0
- package/src/chat/ChatPanelMode.svelte +190 -0
- package/src/chat/ChatPanelState.svelte.ts +1 -0
- package/src/chat/ChatPanelToolbar.svelte +436 -0
- package/src/chat/ChatSurface.svelte +168 -0
- package/src/chat/ContextUsageStatus.svelte +97 -0
- package/src/chat/MessageItem.svelte +111 -214
- package/src/chat/ModelEffortPicker.svelte +326 -0
- package/src/chat/PickerEffort.svelte +71 -0
- package/src/chat/ReasoningControl.svelte +184 -0
- package/src/chat/ReasoningSlider.svelte +174 -0
- package/src/chat/SkillPicker.svelte +14 -6
- package/src/chat/UsageControl.svelte +223 -0
- package/src/chat/UsageDetails.svelte +44 -90
- package/src/chat/chat-attachments.svelte.ts +78 -0
- package/src/chat/chat-panel-history.svelte.ts +430 -0
- package/src/chat/chat-panel-models.svelte.ts +97 -0
- package/src/chat/chat-panel-props.ts +38 -0
- package/src/chat/context-status.ts +101 -0
- package/src/chat/icons/LICENSE-REMIX +186 -0
- package/src/chat/icons/LICENSE-SIMPLE-ICONS +30 -0
- package/src/chat/icons/PathIcon.svelte +8 -0
- package/src/chat/icons/effort-icons.ts +65 -0
- package/src/chat/icons/status-icons.ts +105 -0
- package/src/chat/index.ts +25 -1
- package/src/chat/model-effort.ts +177 -0
- package/src/chat/run-session-history.ts +56 -0
- package/src/chat/run-session-types.ts +69 -0
- package/src/chat/run-session.svelte.ts +73 -135
- package/src/chat/test-support/chat-fixture.ts +194 -0
- package/src/chat/test-support/conversation-fixtures.ts +32 -0
- package/src/chat/test-support/run-session-fixture.ts +112 -0
- package/src/dev/Demo.svelte +3 -1
- package/src/i18n/en-chat.d.ts +1 -0
- package/src/i18n/en-chat.js +201 -0
- package/src/i18n/en.ts +58 -180
- package/src/i18n/index.ts +7 -2
- package/src/i18n/zh-CN-chat.d.ts +1 -0
- package/src/i18n/zh-CN-chat.js +190 -0
- package/src/i18n/zh-CN.ts +8 -153
- package/src/logs/LogPanel.svelte +9 -3
- package/src/logs/LogSettings.svelte +3 -1
- package/src/mcp/McpClientEditor.svelte +204 -0
- package/src/mcp/McpClientSettings.svelte +57 -130
- package/src/mcp/McpServerSettings.svelte +47 -11
- package/src/mcp/input.ts +2 -1
- package/src/ports/contract.ts +11 -2
- package/src/ports/fake-optional.ts +81 -0
- package/src/ports/fake.ts +90 -86
- package/src/ports/search.ts +11 -2
- package/src/ports/test-support/fake-fixture.ts +63 -0
- package/src/providers/EndpointKeysDialog.svelte +27 -13
- package/src/providers/EndpointModels.svelte +311 -0
- package/src/providers/EndpointPanel.svelte +33 -258
- package/src/providers/ModelPickerDialog.svelte +59 -16
- package/src/providers/ModelPricing.svelte +125 -0
- package/src/providers/ModelSelect.svelte +22 -8
- package/src/providers/ModelSettingsDialog.svelte +61 -126
- package/src/providers/ProviderDialogs.svelte +120 -0
- package/src/providers/ProviderList.svelte +159 -0
- package/src/providers/ProvidersSettings.svelte +67 -207
- package/src/providers/RequestConfigDialog.svelte +40 -12
- package/src/providers/SecretInput.svelte +11 -3
- package/src/providers/draft.ts +39 -13
- package/src/providers/edit-session.ts +31 -7
- package/src/providers/reasoning.ts +18 -4
- package/src/providers/revision.svelte.ts +3 -1
- package/src/providers/test-support/providers-fixture.ts +120 -0
- package/src/search/SearchAdvancedTools.svelte +182 -0
- package/src/search/SearchProviderCard.svelte +195 -0
- package/src/search/SearchSettings.svelte +62 -265
- package/src/search/settings-flow.ts +8 -3
- package/src/skills/SkillAdd.svelte +51 -18
- package/src/skills/SkillAuthoring.svelte +69 -20
- package/src/skills/SkillEditor.svelte +13 -3
- package/src/skills/SkillPreviewDialog.svelte +19 -5
- package/src/skills/SkillsSettings.svelte +11 -4
- package/src/skills/test-support/skills-fixture.ts +139 -0
- package/src/tauri/index.ts +23 -7
- package/src/web/index.ts +90 -25
|
@@ -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>
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte'
|
|
3
|
-
import ActivityIcon from '@lucide/svelte/icons/activity'
|
|
4
|
-
import ListIcon from '@lucide/svelte/icons/list'
|
|
5
|
-
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
|
|
6
|
-
import ExternalLinkIcon from '@lucide/svelte/icons/external-link'
|
|
7
3
|
import { Button } from '#lib/components/ui/button/index.js'
|
|
8
4
|
import { Input } from '#lib/components/ui/input/index.js'
|
|
9
|
-
import CopyInput from '../CopyInput.svelte'
|
|
10
|
-
import { Textarea } from '#lib/components/ui/textarea/index.js'
|
|
11
5
|
import * as Alert from '#lib/components/ui/alert/index.js'
|
|
12
6
|
import * as Empty from '#lib/components/ui/empty/index.js'
|
|
13
7
|
import * as Field from '#lib/components/ui/field/index.js'
|
|
8
|
+
import SearchProviderCard from './SearchProviderCard.svelte'
|
|
9
|
+
import SearchAdvancedTools from './SearchAdvancedTools.svelte'
|
|
14
10
|
import { getKitContext } from '../context'
|
|
15
11
|
import { isKitClientError } from '../ports'
|
|
16
12
|
import { createClientSession } from '../session'
|
|
@@ -67,7 +63,8 @@
|
|
|
67
63
|
let passwordRevision = 0
|
|
68
64
|
|
|
69
65
|
const keyOf = (cap: SearchCap, provider: string) => `${cap}:${provider}`
|
|
70
|
-
const errorText = (error: unknown) =>
|
|
66
|
+
const errorText = (error: unknown) =>
|
|
67
|
+
isKitClientError(error) ? t(`errors.${error.code}`) : t('errors.internal')
|
|
71
68
|
const activeProvider = (cap: SearchCap) =>
|
|
72
69
|
settings[cap]?.providers.find((p) => p.id === settings[cap]?.defaultProvider)
|
|
73
70
|
const dialogProvider = $derived.by(() => {
|
|
@@ -85,20 +82,12 @@
|
|
|
85
82
|
function setError(key: string, value: string) {
|
|
86
83
|
errors = { ...errors, [key]: value }
|
|
87
84
|
}
|
|
88
|
-
function safeUrl(raw?: string) {
|
|
89
|
-
if (!raw) return null
|
|
90
|
-
try {
|
|
91
|
-
const url = new URL(raw)
|
|
92
|
-
return ['https:', 'http:'].includes(url.protocol) ? url.href : null
|
|
93
|
-
} catch {
|
|
94
|
-
return null
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
85
|
function patchDraft(cap: SearchCap, provider: string, patch: Draft) {
|
|
98
86
|
const key = keyOf(cap, provider)
|
|
99
87
|
drafts = { ...drafts, [key]: { ...drafts[key], ...patch } }
|
|
100
88
|
const revisions = draftRevisions[key] ?? {}
|
|
101
|
-
for (const field of Object.keys(patch) as (keyof Draft)[])
|
|
89
|
+
for (const field of Object.keys(patch) as (keyof Draft)[])
|
|
90
|
+
revisions[field] = (revisions[field] ?? 0) + 1
|
|
102
91
|
draftRevisions[key] = revisions
|
|
103
92
|
checks = { ...checks, [key]: undefined }
|
|
104
93
|
}
|
|
@@ -168,7 +157,8 @@
|
|
|
168
157
|
onConfigSaved(sent) {
|
|
169
158
|
const next = { ...drafts[key] }
|
|
170
159
|
for (const field of ['baseUrl', 'basicAuthUsername'] as const) {
|
|
171
|
-
if (sent[field] !== undefined && draftRevisions[key]?.[field] === revisions[field])
|
|
160
|
+
if (sent[field] !== undefined && draftRevisions[key]?.[field] === revisions[field])
|
|
161
|
+
delete next[field]
|
|
172
162
|
}
|
|
173
163
|
drafts = { ...drafts, [key]: next }
|
|
174
164
|
},
|
|
@@ -276,7 +266,8 @@
|
|
|
276
266
|
if (isCurrent()) notify.toast('success', t('search.copied'))
|
|
277
267
|
} else revealed = { ...revealed, [key.id]: value }
|
|
278
268
|
} catch (error) {
|
|
279
|
-
if (isCurrent())
|
|
269
|
+
if (isCurrent())
|
|
270
|
+
dialogError = copy && !isKitClientError(error) ? t('search.copyFailed') : errorText(error)
|
|
280
271
|
} finally {
|
|
281
272
|
if (isCurrent() && passwordRevision === revision) {
|
|
282
273
|
password = ''
|
|
@@ -362,250 +353,47 @@
|
|
|
362
353
|
<div class="mx-auto flex w-full min-w-0 max-w-3xl flex-col gap-4">
|
|
363
354
|
{#each caps as cap (cap)}
|
|
364
355
|
{@const provider = activeProvider(cap)}
|
|
365
|
-
{
|
|
356
|
+
{@const current = settings[cap]}
|
|
357
|
+
{#if provider && current}
|
|
366
358
|
{@const key = keyOf(cap, provider.id)}
|
|
367
|
-
<
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
disabled={pending[cap]}
|
|
387
|
-
value={settings[cap]?.defaultProvider}
|
|
388
|
-
onchange={(event) => void choose(cap, event.currentTarget.value)}
|
|
389
|
-
>
|
|
390
|
-
{#each settings[cap]?.providers ?? [] as option (option.id)}<option value={option.id}
|
|
391
|
-
>{option.name}</option
|
|
392
|
-
>{/each}
|
|
393
|
-
</select>
|
|
394
|
-
</header>
|
|
395
|
-
{#if errors[cap]}<Alert.Root variant="destructive"
|
|
396
|
-
><Alert.Description>{errors[cap]}</Alert.Description></Alert.Root
|
|
397
|
-
>{/if}
|
|
398
|
-
<Field.Group class="gap-3 border-t pt-3">
|
|
399
|
-
{#if provider.keyMode !== 'none' || provider.basicAuth}
|
|
400
|
-
<Field.Field data-disabled={pending[cap]}>
|
|
401
|
-
<div class="flex flex-wrap items-baseline gap-2">
|
|
402
|
-
<Field.Label for={`search-key-${cap}`}
|
|
403
|
-
>{provider.basicAuth ? t('search.password') : t('search.apiKey')}</Field.Label
|
|
404
|
-
>
|
|
405
|
-
{#if safeUrl(provider.apiKeyWebsite)}<a
|
|
406
|
-
class="text-primary text-kit-sm hover:underline"
|
|
407
|
-
href={safeUrl(provider.apiKeyWebsite) ?? ''}
|
|
408
|
-
target="_blank"
|
|
409
|
-
rel="noopener noreferrer">{t('search.apiKeyWebsite')}</a
|
|
410
|
-
>{/if}
|
|
411
|
-
</div>
|
|
412
|
-
<div class="flex min-w-0 items-center gap-2">
|
|
413
|
-
<Input
|
|
414
|
-
id={`search-key-${cap}`}
|
|
415
|
-
class="min-w-0 flex-1"
|
|
416
|
-
type="password"
|
|
417
|
-
autocomplete="off"
|
|
418
|
-
spellcheck={false}
|
|
419
|
-
maxlength={4096}
|
|
420
|
-
disabled={pending[cap]}
|
|
421
|
-
placeholder={t('search.newKey')}
|
|
422
|
-
value={keyDrafts[key] ?? ''}
|
|
423
|
-
oninput={(event) => {
|
|
424
|
-
keyDrafts = { ...keyDrafts, [key]: event.currentTarget.value }
|
|
425
|
-
keyRevisions[key] = (keyRevisions[key] ?? 0) + 1
|
|
426
|
-
checks = { ...checks, [key]: undefined }
|
|
427
|
-
}}
|
|
428
|
-
/>
|
|
429
|
-
<Button
|
|
430
|
-
type="button"
|
|
431
|
-
variant="outline"
|
|
432
|
-
size="icon-sm"
|
|
433
|
-
disabled={pending[cap]}
|
|
434
|
-
aria-label={t('search.manageKeys')}
|
|
435
|
-
title={t('search.manageKeys')}
|
|
436
|
-
onclick={() => openKeys(cap, provider.id)}><ListIcon /></Button
|
|
437
|
-
>
|
|
438
|
-
<Button
|
|
439
|
-
type="button"
|
|
440
|
-
variant="outline"
|
|
441
|
-
size="icon-sm"
|
|
442
|
-
disabled={pending[cap]}
|
|
443
|
-
aria-label={t('search.check')}
|
|
444
|
-
title={t('search.check')}
|
|
445
|
-
onclick={() => void saveAndCheck(cap, provider, true)}
|
|
446
|
-
>{#if pending[cap]}<LoaderCircleIcon class="animate-spin" />{:else}<ActivityIcon />{/if}</Button
|
|
447
|
-
>
|
|
448
|
-
</div>
|
|
449
|
-
<Field.Description
|
|
450
|
-
>{provider.keys.length
|
|
451
|
-
? provider.keys.map((item) => item.masked).join(' · ')
|
|
452
|
-
: t('search.keyNotSet')}</Field.Description
|
|
453
|
-
>
|
|
454
|
-
</Field.Field>
|
|
455
|
-
{/if}
|
|
456
|
-
{#if provider.defaultBaseUrl !== undefined}
|
|
457
|
-
<Field.Field data-disabled={pending[cap]}>
|
|
458
|
-
<Field.Label for={`search-url-${cap}`}>{t('search.baseUrl')}</Field.Label>
|
|
459
|
-
<div class="flex min-w-0 items-center gap-2">
|
|
460
|
-
<CopyInput
|
|
461
|
-
id={`search-url-${cap}`}
|
|
462
|
-
class="min-w-0 flex-1"
|
|
463
|
-
disabled={pending[cap]}
|
|
464
|
-
value={draftOf(cap, provider.id).baseUrl ?? provider.baseUrl ?? provider.defaultBaseUrl}
|
|
465
|
-
oninput={(event) => patchDraft(cap, provider.id, { baseUrl: event.currentTarget.value })}
|
|
466
|
-
/>
|
|
467
|
-
{#if provider.keyMode === 'none' && !provider.basicAuth}<Button
|
|
468
|
-
type="button"
|
|
469
|
-
variant="outline"
|
|
470
|
-
size="icon-sm"
|
|
471
|
-
disabled={pending[cap]}
|
|
472
|
-
aria-label={t('search.check')}
|
|
473
|
-
title={t('search.check')}
|
|
474
|
-
onclick={() => void saveAndCheck(cap, provider, true)}><ActivityIcon /></Button
|
|
475
|
-
>{/if}
|
|
476
|
-
</div>
|
|
477
|
-
</Field.Field>
|
|
478
|
-
{/if}
|
|
479
|
-
{#if provider.basicAuth}
|
|
480
|
-
<Field.Field
|
|
481
|
-
><Field.Label for={`search-user-${cap}`}>{t('search.username')}</Field.Label><Input
|
|
482
|
-
id={`search-user-${cap}`}
|
|
483
|
-
disabled={pending[cap]}
|
|
484
|
-
value={draftOf(cap, provider.id).basicAuthUsername ?? provider.basicAuthUsername ?? ''}
|
|
485
|
-
oninput={(event) => patchDraft(cap, provider.id, { basicAuthUsername: event.currentTarget.value })}
|
|
486
|
-
/></Field.Field
|
|
487
|
-
>
|
|
488
|
-
{/if}
|
|
489
|
-
</Field.Group>
|
|
490
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
491
|
-
{#if provider.defaultBaseUrl !== undefined || provider.keyMode !== 'none'}<Button
|
|
492
|
-
type="button"
|
|
493
|
-
size="sm"
|
|
494
|
-
disabled={pending[cap]}
|
|
495
|
-
onclick={() => void saveAndCheck(cap, provider)}>{t('search.save')}</Button
|
|
496
|
-
>{/if}
|
|
497
|
-
{#if pending[cap]}<span class="text-muted-foreground text-kit-sm" role="status">{t('search.checking')}</span
|
|
498
|
-
>{:else if checks[key] !== undefined}<span class="text-muted-foreground text-kit-sm" role="status"
|
|
499
|
-
>{t('search.resultCount', { count: checks[key] ?? 0 })}</span
|
|
500
|
-
>{/if}
|
|
501
|
-
</div>
|
|
502
|
-
</section>
|
|
359
|
+
<SearchProviderCard
|
|
360
|
+
{cap}
|
|
361
|
+
{provider}
|
|
362
|
+
settings={current}
|
|
363
|
+
pending={pending[cap] === true}
|
|
364
|
+
error={errors[cap] ?? ''}
|
|
365
|
+
draft={draftOf(cap, provider.id)}
|
|
366
|
+
keyDraft={keyDrafts[key] ?? ''}
|
|
367
|
+
check={checks[key]}
|
|
368
|
+
onChoose={(next) => void choose(cap, next)}
|
|
369
|
+
onOpenKeys={() => openKeys(cap, provider.id)}
|
|
370
|
+
onKeyInput={(value) => {
|
|
371
|
+
keyDrafts = { ...keyDrafts, [key]: value }
|
|
372
|
+
keyRevisions[key] = (keyRevisions[key] ?? 0) + 1
|
|
373
|
+
checks = { ...checks, [key]: undefined }
|
|
374
|
+
}}
|
|
375
|
+
onPatchDraft={(patch) => patchDraft(cap, provider.id, patch)}
|
|
376
|
+
onSave={(check = false) => void saveAndCheck(cap, provider, check)}
|
|
377
|
+
/>
|
|
503
378
|
{/if}
|
|
504
379
|
{/each}
|
|
505
|
-
<
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
id="search-compression"
|
|
523
|
-
class="bg-background border-input h-kit-control ml-auto rounded-kit-sm border px-2 text-kit-sm"
|
|
524
|
-
disabled={pending.preferences}
|
|
525
|
-
bind:value={preferences.compression}
|
|
526
|
-
><option value="none">{t('search.compressionNone')}</option><option value="cutoff"
|
|
527
|
-
>{t('search.compressionCutoff')}</option
|
|
528
|
-
></select
|
|
529
|
-
></Field.Field
|
|
530
|
-
>
|
|
531
|
-
{#if preferences.compression === 'cutoff'}<Field.Field orientation="horizontal"
|
|
532
|
-
><Field.Label for="search-cutoff">{t('search.cutoffLimit')}</Field.Label><Input
|
|
533
|
-
id="search-cutoff"
|
|
534
|
-
class="ml-auto w-24"
|
|
535
|
-
type="number"
|
|
536
|
-
min="100"
|
|
537
|
-
max="20000"
|
|
538
|
-
disabled={pending.preferences}
|
|
539
|
-
bind:value={preferences.cutoffLimit}
|
|
540
|
-
/></Field.Field
|
|
541
|
-
>{/if}
|
|
542
|
-
<Field.Field
|
|
543
|
-
><Field.Label for="search-blacklist">{t('search.blacklist')}</Field.Label><Textarea
|
|
544
|
-
id="search-blacklist"
|
|
545
|
-
rows={3}
|
|
546
|
-
disabled={pending.preferences}
|
|
547
|
-
placeholder="example.com\n*://*.example.org/*"
|
|
548
|
-
bind:value={blacklist}
|
|
549
|
-
/></Field.Field
|
|
550
|
-
>
|
|
551
|
-
</Field.Group>
|
|
552
|
-
</details>
|
|
553
|
-
<Field.Field class="mt-3 border-t pt-3"
|
|
554
|
-
><label class="flex items-center gap-2 text-kit-sm"
|
|
555
|
-
><input type="checkbox" disabled={pending.preferences} bind:checked={preferences.modelToolsPreferred} />{t(
|
|
556
|
-
'search.nativePreferred',
|
|
557
|
-
)}</label
|
|
558
|
-
><Field.Description>{t('search.nativePrivacy')}</Field.Description></Field.Field
|
|
559
|
-
>
|
|
560
|
-
{#if errors.preferences}<p role="alert" class="text-destructive text-kit-sm">{errors.preferences}</p>{/if}
|
|
561
|
-
<Button class="mt-3" type="button" size="sm" disabled={pending.preferences} onclick={() => void savePreferences()}
|
|
562
|
-
>{t('search.save')}</Button
|
|
563
|
-
>
|
|
564
|
-
</section>
|
|
565
|
-
<section class="flex min-w-0 flex-col gap-3 rounded-kit border p-4" aria-label={t('search.tools')}>
|
|
566
|
-
<Field.Field
|
|
567
|
-
><Field.Label for="search-query">{t('search.query')}</Field.Label>
|
|
568
|
-
<div class="flex min-w-0 gap-2">
|
|
569
|
-
<Input id="search-query" class="min-w-0 flex-1" disabled={pending.query} bind:value={query} /><Button
|
|
570
|
-
type="button"
|
|
571
|
-
disabled={pending.query || !query.trim()}
|
|
572
|
-
onclick={() => void search()}>{t('search.runSearch')}</Button
|
|
573
|
-
>
|
|
574
|
-
</div></Field.Field
|
|
575
|
-
>
|
|
576
|
-
{#if errors.query}<p class="text-destructive text-kit-sm" role="alert">{errors.query}</p>{/if}
|
|
577
|
-
{#if result}<p class="text-muted-foreground text-kit-sm">
|
|
578
|
-
{t('search.resultCount', { count: result.hits.length })}
|
|
579
|
-
</p>
|
|
580
|
-
{#each result.hits as hit (hit.url)}<article class="min-w-0 text-kit-sm">
|
|
581
|
-
<a
|
|
582
|
-
class="text-primary break-words hover:underline"
|
|
583
|
-
href={safeUrl(hit.url) ?? ''}
|
|
584
|
-
target="_blank"
|
|
585
|
-
rel="noopener noreferrer">{hit.title}</a
|
|
586
|
-
>
|
|
587
|
-
<p class="whitespace-pre-wrap break-words">{hit.snippet}</p>
|
|
588
|
-
</article>{/each}{/if}
|
|
589
|
-
<Field.Field
|
|
590
|
-
><Field.Label for="search-page-url">{t('search.pageUrl')}</Field.Label>
|
|
591
|
-
<div class="flex min-w-0 gap-2">
|
|
592
|
-
<Input id="search-page-url" class="min-w-0 flex-1" disabled={pending.page} bind:value={pageUrl} /><Button
|
|
593
|
-
type="button"
|
|
594
|
-
disabled={pending.page || !pageUrl.trim()}
|
|
595
|
-
onclick={() => void readPage()}>{t('search.read')}</Button
|
|
596
|
-
>
|
|
597
|
-
</div></Field.Field
|
|
598
|
-
>
|
|
599
|
-
{#if errors.page}<p class="text-destructive text-kit-sm" role="alert">{errors.page}</p>{/if}
|
|
600
|
-
{#if page}<h3 class="text-kit-base font-medium">{page.title}</h3>
|
|
601
|
-
<a
|
|
602
|
-
class="text-primary break-words text-kit-sm"
|
|
603
|
-
href={safeUrl(page.url) ?? ''}
|
|
604
|
-
target="_blank"
|
|
605
|
-
rel="noopener noreferrer">{page.url}</a
|
|
606
|
-
>
|
|
607
|
-
<pre class="max-h-96 overflow-auto whitespace-pre-wrap break-words text-kit-sm">{page.content}</pre>{/if}
|
|
608
|
-
</section>
|
|
380
|
+
<SearchAdvancedTools
|
|
381
|
+
bind:preferences
|
|
382
|
+
bind:blacklist
|
|
383
|
+
bind:query
|
|
384
|
+
bind:pageUrl
|
|
385
|
+
preferencesPending={pending.preferences === true}
|
|
386
|
+
preferencesError={errors.preferences ?? ''}
|
|
387
|
+
{result}
|
|
388
|
+
{page}
|
|
389
|
+
queryPending={pending.query === true}
|
|
390
|
+
pagePending={pending.page === true}
|
|
391
|
+
queryError={errors.query ?? ''}
|
|
392
|
+
pageError={errors.page ?? ''}
|
|
393
|
+
onSavePreferences={() => void savePreferences()}
|
|
394
|
+
onSearch={() => void search()}
|
|
395
|
+
onReadPage={() => void readPage()}
|
|
396
|
+
/>
|
|
609
397
|
</div>
|
|
610
398
|
{/if}
|
|
611
399
|
|
|
@@ -665,15 +453,21 @@
|
|
|
665
453
|
disabled={dialogBusy || (port?.revealNeedsPassword && !password)}
|
|
666
454
|
onclick={() => void revealKey(key, true)}>{t('search.copy')}</Button
|
|
667
455
|
>
|
|
668
|
-
<Button
|
|
669
|
-
|
|
456
|
+
<Button
|
|
457
|
+
type="button"
|
|
458
|
+
variant="ghost"
|
|
459
|
+
size="sm"
|
|
460
|
+
disabled={dialogBusy}
|
|
461
|
+
onclick={() => void removeKey(key)}>{t('search.delete')}</Button
|
|
670
462
|
>
|
|
671
463
|
</div>
|
|
672
464
|
</li>
|
|
673
465
|
{/each}
|
|
674
466
|
</ul>
|
|
675
467
|
<Field.Field
|
|
676
|
-
><Field.Label for="search-edit-key"
|
|
468
|
+
><Field.Label for="search-edit-key"
|
|
469
|
+
>{t(editingId ? 'search.editKey' : 'search.newKey')}</Field.Label
|
|
470
|
+
><Input
|
|
677
471
|
id="search-edit-key"
|
|
678
472
|
type="password"
|
|
679
473
|
autocomplete="off"
|
|
@@ -692,8 +486,11 @@
|
|
|
692
486
|
<Button
|
|
693
487
|
type="button"
|
|
694
488
|
size="sm"
|
|
695
|
-
disabled={dialogBusy ||
|
|
696
|
-
|
|
489
|
+
disabled={dialogBusy ||
|
|
490
|
+
!editValue.trim() ||
|
|
491
|
+
(!editingId && dialogProvider.keys.length >= 16)}
|
|
492
|
+
onclick={() => void saveEditedKey()}
|
|
493
|
+
>{t(editingId ? 'search.save' : 'search.addKey')}</Button
|
|
697
494
|
><Button type="button" variant="outline" size="sm" onclick={() => keyDialog.close()}
|
|
698
495
|
>{t('common.cancel')}</Button
|
|
699
496
|
>
|
|
@@ -17,7 +17,8 @@ export async function saveSearchSettings(
|
|
|
17
17
|
const config = { ...input.config }
|
|
18
18
|
const key = input.key ? { ...input.key } : undefined
|
|
19
19
|
const check = input.check
|
|
20
|
-
const cancelled = () =>
|
|
20
|
+
const cancelled = () =>
|
|
21
|
+
new KitClientError({ code: 'cancelled', message: 'Search settings cancelled', details: {} })
|
|
21
22
|
function assertCurrent() {
|
|
22
23
|
if (options?.signal?.aborted || !session.isCurrent()) throw cancelled()
|
|
23
24
|
}
|
|
@@ -51,12 +52,16 @@ export async function saveSearchSettings(
|
|
|
51
52
|
assertCurrent()
|
|
52
53
|
session.onConfigSaved(config)
|
|
53
54
|
if (key?.value.trim()) {
|
|
54
|
-
settings = await step(() =>
|
|
55
|
+
settings = await step(() =>
|
|
56
|
+
port.saveKey(config.capability, config.provider, key.value, key.keyId, options),
|
|
57
|
+
)
|
|
55
58
|
session.onSettings(settings)
|
|
56
59
|
assertCurrent()
|
|
57
60
|
session.onKeySaved()
|
|
58
61
|
}
|
|
59
|
-
const checked = check
|
|
62
|
+
const checked = check
|
|
63
|
+
? await step(() => port.check(config.capability, config.provider, options))
|
|
64
|
+
: undefined
|
|
60
65
|
assertCurrent()
|
|
61
66
|
return checked ? { settings, checked } : { settings }
|
|
62
67
|
}
|