@cat-factory/app 0.46.12 → 0.47.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.
- package/app/components/auth/AuthGate.vue +1 -1
- package/app/components/auth/LoginScreen.vue +131 -1
- package/app/components/layout/AccountDeploymentSettings.vue +232 -5
- package/app/components/providers/AiPresetMismatchDialog.vue +26 -13
- package/app/components/providers/AiProviderOnboardingModal.vue +18 -15
- package/app/components/providers/ApiKeysSection.vue +105 -63
- package/app/components/providers/PersonalCredentialModal.vue +35 -29
- package/app/components/providers/PersonalSubscriptionSection.vue +17 -2
- package/app/components/providers/SignInRequiredNotice.vue +19 -0
- package/app/components/providers/VendorCredentialsModal.vue +82 -39
- package/app/components/settings/AccountSettingsPanel.vue +9 -6
- package/app/components/settings/IssueTrackerPanel.vue +114 -53
- package/app/components/settings/LocalModeSettingsPanel.vue +60 -28
- package/app/components/settings/LocalModelEndpointsPanel.vue +72 -27
- package/app/components/settings/MergeThresholdsPanel.vue +93 -45
- package/app/components/settings/ModelConfigurationPanel.vue +62 -36
- package/app/components/settings/ObservabilityConnectionPanel.vue +65 -35
- package/app/components/settings/OpenRouterCatalogPanel.vue +70 -40
- package/app/components/settings/ProviderConnectionPanel.vue +115 -61
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +9 -12
- package/app/components/settings/UserSecretsSection.vue +37 -18
- package/app/components/settings/WorkspaceSettingsPanel.vue +114 -62
- package/app/composables/api/auth.ts +7 -0
- package/app/stores/auth.ts +25 -1
- package/app/types/accountSettings.ts +4 -0
- package/i18n/locales/en.json +663 -0
- package/i18n/locales/es.json +651 -0
- package/i18n/locales/fr.json +651 -0
- package/i18n/locales/pl.json +651 -0
- package/i18n/locales/uk.json +651 -0
- package/package.json +2 -2
|
@@ -1,10 +1,74 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref } from 'vue'
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
3
|
import { apiErrorEnvelope } from '~/composables/api/errors'
|
|
4
4
|
|
|
5
5
|
const auth = useAuthStore()
|
|
6
6
|
const { t } = useI18n()
|
|
7
7
|
|
|
8
|
+
// Local-mode source-control PAT login. GitHub/GitLab are brand names (kept verbatim across
|
|
9
|
+
// locales), as are the token-settings URLs, so they're inline constants rather than catalog
|
|
10
|
+
// keys — same convention as the provider descriptors in ApiKeysSection. The actual link
|
|
11
|
+
// prefers the server's scopes-preselected deep link (`patLogin.setupUrls`); these are the
|
|
12
|
+
// fallback when it's absent.
|
|
13
|
+
type PatProvider = 'github' | 'gitlab'
|
|
14
|
+
const PROVIDER_LABELS: Record<PatProvider, string> = { github: 'GitHub', gitlab: 'GitLab' }
|
|
15
|
+
const PROVIDER_ICONS: Record<PatProvider, string> = {
|
|
16
|
+
github: 'i-lucide-github',
|
|
17
|
+
gitlab: 'i-lucide-gitlab',
|
|
18
|
+
}
|
|
19
|
+
// Fallback token-creation pages, used only if the server didn't advertise a deep link.
|
|
20
|
+
const PROVIDER_TOKEN_URLS: Record<PatProvider, string> = {
|
|
21
|
+
github: 'https://github.com/settings/tokens/new',
|
|
22
|
+
gitlab: 'https://gitlab.com/-/user_settings/personal_access_tokens',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const patLoginCfg = computed(() => auth.localMode?.patLogin)
|
|
26
|
+
const configuredProviders = computed<PatProvider[]>(
|
|
27
|
+
() => (patLoginCfg.value?.configured ?? []) as PatProvider[],
|
|
28
|
+
)
|
|
29
|
+
const availableProviders = computed<PatProvider[]>(
|
|
30
|
+
() => (patLoginCfg.value?.available ?? []) as PatProvider[],
|
|
31
|
+
)
|
|
32
|
+
const showLocalLogin = computed(() => availableProviders.value.length > 0)
|
|
33
|
+
|
|
34
|
+
const patProvider = ref<PatProvider>('github')
|
|
35
|
+
const patToken = ref('')
|
|
36
|
+
const patBusy = ref(false)
|
|
37
|
+
const patError = ref<string | null>(null)
|
|
38
|
+
|
|
39
|
+
// Keep the picker on an actually-available provider.
|
|
40
|
+
watch(
|
|
41
|
+
availableProviders,
|
|
42
|
+
(list) => {
|
|
43
|
+
if (list.length && !list.includes(patProvider.value)) patProvider.value = list[0]!
|
|
44
|
+
},
|
|
45
|
+
{ immediate: true },
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const patProviderItems = computed(() =>
|
|
49
|
+
availableProviders.value.map((p) => ({ label: PROVIDER_LABELS[p], value: p })),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Prefer the server's scopes-preselected deep link (it owns the per-provider scopes);
|
|
53
|
+
// fall back to the plain token page if it wasn't advertised.
|
|
54
|
+
const tokenCreateUrl = computed(
|
|
55
|
+
() => patLoginCfg.value?.setupUrls?.[patProvider.value] ?? PROVIDER_TOKEN_URLS[patProvider.value],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
/** One-click (configured PAT) or pasted-token sign-in; reloads so the app boots signed in. */
|
|
59
|
+
async function submitPat(provider: PatProvider, token?: string) {
|
|
60
|
+
patError.value = null
|
|
61
|
+
patBusy.value = true
|
|
62
|
+
try {
|
|
63
|
+
await auth.patLogin(token ? { provider, token } : { provider })
|
|
64
|
+
if (typeof window !== 'undefined') window.location.assign(window.location.pathname)
|
|
65
|
+
} catch (e) {
|
|
66
|
+
patError.value = apiErrorEnvelope(e)?.message ?? t('auth.localMode.failed')
|
|
67
|
+
} finally {
|
|
68
|
+
patBusy.value = false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
8
72
|
// An invite token may ride in on the URL (?invite=…) — it flows through the OAuth
|
|
9
73
|
// redirect and the password signup so a brand-new user can join the org on first login.
|
|
10
74
|
const invite = computed(() => {
|
|
@@ -90,6 +154,72 @@ const showOAuthDivider = computed(
|
|
|
90
154
|
</p>
|
|
91
155
|
</div>
|
|
92
156
|
|
|
157
|
+
<!-- Local mode: sign in with a source-control PAT (no OAuth round-trip needed) -->
|
|
158
|
+
<div v-if="showLocalLogin && mode !== 'forgot'" class="space-y-3">
|
|
159
|
+
<!-- One-click: a PAT is already configured server-side -->
|
|
160
|
+
<UButton
|
|
161
|
+
v-for="p in configuredProviders"
|
|
162
|
+
:key="p"
|
|
163
|
+
block
|
|
164
|
+
size="lg"
|
|
165
|
+
color="primary"
|
|
166
|
+
:icon="PROVIDER_ICONS[p]"
|
|
167
|
+
:loading="patBusy"
|
|
168
|
+
@click="submitPat(p)"
|
|
169
|
+
>
|
|
170
|
+
{{ t('auth.localMode.continueWith', { provider: PROVIDER_LABELS[p] }) }}
|
|
171
|
+
</UButton>
|
|
172
|
+
|
|
173
|
+
<!-- Enter a PAT inline -->
|
|
174
|
+
<form class="space-y-2" @submit.prevent="submitPat(patProvider, patToken.trim())">
|
|
175
|
+
<p class="text-xs font-medium text-slate-400">{{ t('auth.localMode.enterPatTitle') }}</p>
|
|
176
|
+
<USelect
|
|
177
|
+
v-if="patProviderItems.length > 1"
|
|
178
|
+
v-model="patProvider"
|
|
179
|
+
:items="patProviderItems"
|
|
180
|
+
size="lg"
|
|
181
|
+
class="w-full"
|
|
182
|
+
/>
|
|
183
|
+
<UTextarea
|
|
184
|
+
v-model="patToken"
|
|
185
|
+
:rows="2"
|
|
186
|
+
:placeholder="
|
|
187
|
+
t('auth.localMode.tokenPlaceholder', { provider: PROVIDER_LABELS[patProvider] })
|
|
188
|
+
"
|
|
189
|
+
class="w-full font-mono"
|
|
190
|
+
/>
|
|
191
|
+
<div class="flex items-center justify-between gap-2">
|
|
192
|
+
<a
|
|
193
|
+
:href="tokenCreateUrl"
|
|
194
|
+
target="_blank"
|
|
195
|
+
rel="noopener noreferrer"
|
|
196
|
+
class="text-xs text-indigo-400 hover:underline"
|
|
197
|
+
>
|
|
198
|
+
{{ t('auth.localMode.createToken', { provider: PROVIDER_LABELS[patProvider] }) }}
|
|
199
|
+
</a>
|
|
200
|
+
<UButton
|
|
201
|
+
size="lg"
|
|
202
|
+
color="neutral"
|
|
203
|
+
variant="subtle"
|
|
204
|
+
type="submit"
|
|
205
|
+
:loading="patBusy"
|
|
206
|
+
:disabled="!patToken.trim()"
|
|
207
|
+
>
|
|
208
|
+
{{ t('auth.localMode.submit') }}
|
|
209
|
+
</UButton>
|
|
210
|
+
</div>
|
|
211
|
+
</form>
|
|
212
|
+
<p v-if="patError" class="text-sm text-rose-400">{{ patError }}</p>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div
|
|
216
|
+
v-if="showLocalLogin && auth.providers.password && mode !== 'forgot'"
|
|
217
|
+
class="my-4 flex items-center gap-3 text-xs text-slate-500"
|
|
218
|
+
>
|
|
219
|
+
<span class="h-px flex-1 bg-slate-800" /> {{ t('auth.localMode.orDivider') }}
|
|
220
|
+
<span class="h-px flex-1 bg-slate-800" />
|
|
221
|
+
</div>
|
|
222
|
+
|
|
93
223
|
<!-- OAuth providers -->
|
|
94
224
|
<div v-if="mode !== 'forgot'" class="space-y-2">
|
|
95
225
|
<UButton
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
3
|
+
import type { ContentStorageBackend, ContentStorageConfig } from '~/types/accountSettings'
|
|
3
4
|
|
|
4
5
|
// Deployment integration secrets for an account (admin only): the Slack app OAuth
|
|
5
|
-
// credentials
|
|
6
|
-
//
|
|
7
|
-
// ever shows whether each integration is
|
|
8
|
-
// blank inputs leave a configured secret
|
|
9
|
-
// wired (no ENCRYPTION_KEY).
|
|
6
|
+
// credentials, the container web-search upstream keys, and the binary-artifact (screenshot)
|
|
7
|
+
// content-storage backend — all moved out of env into the per-account settings store, sealed
|
|
8
|
+
// at rest. Secrets are write-only: the panel only ever shows whether each integration is
|
|
9
|
+
// configured (the `summary`), never the values; blank inputs leave a configured secret
|
|
10
|
+
// unchanged. Hidden when the settings store isn't wired (no ENCRYPTION_KEY).
|
|
10
11
|
const props = defineProps<{ accountId: string }>()
|
|
11
12
|
|
|
12
13
|
const store = useAccountSettingsStore()
|
|
@@ -20,9 +21,54 @@ const savingWeb = ref(false)
|
|
|
20
21
|
|
|
21
22
|
const summary = computed(() => store.view?.summary ?? null)
|
|
22
23
|
|
|
24
|
+
// ---- Content storage (binary artifacts / screenshots) --------------------
|
|
25
|
+
// Exhaustive enum→key map (drift guard tier 2): every backend resolves to a static literal
|
|
26
|
+
// `t()` key, so adding a backend without a label fails the typecheck on this Record.
|
|
27
|
+
const contentBackendLabels = computed<Record<ContentStorageBackend, string>>(() => ({
|
|
28
|
+
off: t('layout.accountDeployment.contentStorage.backends.off'),
|
|
29
|
+
fs: t('layout.accountDeployment.contentStorage.backends.fs'),
|
|
30
|
+
s3: t('layout.accountDeployment.contentStorage.backends.s3'),
|
|
31
|
+
r2: t('layout.accountDeployment.contentStorage.backends.r2'),
|
|
32
|
+
db: t('layout.accountDeployment.contentStorage.backends.db'),
|
|
33
|
+
}))
|
|
34
|
+
const storageCapability = computed(() => store.view?.contentStorageCapability ?? null)
|
|
35
|
+
const storageSummary = computed(() => summary.value?.contentStorage ?? null)
|
|
36
|
+
const backendItems = computed(() =>
|
|
37
|
+
(storageCapability.value?.supportedBackends ?? []).map((b) => ({
|
|
38
|
+
label: contentBackendLabels.value[b],
|
|
39
|
+
value: b,
|
|
40
|
+
})),
|
|
41
|
+
)
|
|
42
|
+
const csBackend = ref<ContentStorageBackend>('off')
|
|
43
|
+
const cs = reactive({
|
|
44
|
+
basePath: '',
|
|
45
|
+
region: '',
|
|
46
|
+
bucket: '',
|
|
47
|
+
prefix: '',
|
|
48
|
+
endpoint: '',
|
|
49
|
+
forcePathStyle: false,
|
|
50
|
+
accessKeyId: '',
|
|
51
|
+
secretAccessKey: '',
|
|
52
|
+
})
|
|
53
|
+
const savingStorage = ref(false)
|
|
54
|
+
|
|
55
|
+
function hydrateStorage() {
|
|
56
|
+
const cfg = store.view?.config?.contentStorage
|
|
57
|
+
csBackend.value = cfg?.backend ?? storageCapability.value?.defaultBackend ?? 'off'
|
|
58
|
+
cs.basePath = cfg?.fs?.basePath ?? ''
|
|
59
|
+
cs.region = cfg?.s3?.region ?? ''
|
|
60
|
+
cs.bucket = cfg?.s3?.bucket ?? ''
|
|
61
|
+
cs.prefix = cfg?.s3?.prefix ?? ''
|
|
62
|
+
cs.endpoint = cfg?.s3?.endpoint ?? ''
|
|
63
|
+
cs.forcePathStyle = cfg?.s3?.forcePathStyle ?? false
|
|
64
|
+
cs.accessKeyId = ''
|
|
65
|
+
cs.secretAccessKey = ''
|
|
66
|
+
}
|
|
67
|
+
|
|
23
68
|
onMounted(async () => {
|
|
24
69
|
try {
|
|
25
70
|
await store.load(props.accountId)
|
|
71
|
+
hydrateStorage()
|
|
26
72
|
} catch (e) {
|
|
27
73
|
toast.add({
|
|
28
74
|
title: t('layout.accountDeployment.loadFailed'),
|
|
@@ -33,6 +79,72 @@ onMounted(async () => {
|
|
|
33
79
|
}
|
|
34
80
|
})
|
|
35
81
|
|
|
82
|
+
async function saveStorage() {
|
|
83
|
+
const backend = csBackend.value
|
|
84
|
+
const config: ContentStorageConfig = { backend }
|
|
85
|
+
if (backend === 'fs' && cs.basePath.trim()) {
|
|
86
|
+
config.fs = { basePath: cs.basePath.trim() }
|
|
87
|
+
}
|
|
88
|
+
if (backend === 's3') {
|
|
89
|
+
if (!cs.region.trim() || !cs.bucket.trim()) {
|
|
90
|
+
toast.add({
|
|
91
|
+
title: t('layout.accountDeployment.contentStorage.regionBucketValidation'),
|
|
92
|
+
color: 'error',
|
|
93
|
+
})
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
config.s3 = {
|
|
97
|
+
region: cs.region.trim(),
|
|
98
|
+
bucket: cs.bucket.trim(),
|
|
99
|
+
...(cs.prefix.trim() ? { prefix: cs.prefix.trim() } : {}),
|
|
100
|
+
...(cs.endpoint.trim() ? { endpoint: cs.endpoint.trim() } : {}),
|
|
101
|
+
...(cs.forcePathStyle ? { forcePathStyle: true } : {}),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const input: Parameters<typeof store.save>[1] = { config: { contentStorage: config } }
|
|
105
|
+
if (backend === 's3') {
|
|
106
|
+
const id = cs.accessKeyId.trim()
|
|
107
|
+
const key = cs.secretAccessKey.trim()
|
|
108
|
+
if (id && key) {
|
|
109
|
+
input.secrets = { s3: { accessKeyId: id, secretAccessKey: key } }
|
|
110
|
+
} else if (id || key) {
|
|
111
|
+
toast.add({
|
|
112
|
+
title: t('layout.accountDeployment.contentStorage.bothKeysValidation'),
|
|
113
|
+
color: 'error',
|
|
114
|
+
})
|
|
115
|
+
return
|
|
116
|
+
} else if (!storageSummary.value?.s3CredentialsConfigured) {
|
|
117
|
+
toast.add({
|
|
118
|
+
title: t('layout.accountDeployment.contentStorage.keysValidation'),
|
|
119
|
+
color: 'error',
|
|
120
|
+
})
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
// else: keys already stored and none re-entered → leave them unchanged.
|
|
124
|
+
} else {
|
|
125
|
+
// Switching off S3: drop any stored S3 credentials.
|
|
126
|
+
input.secrets = { s3: null }
|
|
127
|
+
}
|
|
128
|
+
savingStorage.value = true
|
|
129
|
+
try {
|
|
130
|
+
await store.save(props.accountId, input)
|
|
131
|
+
hydrateStorage()
|
|
132
|
+
toast.add({
|
|
133
|
+
title: t('layout.accountDeployment.contentStorage.saved'),
|
|
134
|
+
icon: 'i-lucide-check',
|
|
135
|
+
color: 'success',
|
|
136
|
+
})
|
|
137
|
+
} catch (e) {
|
|
138
|
+
toast.add({
|
|
139
|
+
title: t('layout.accountDeployment.contentStorage.saveFailed'),
|
|
140
|
+
description: e instanceof Error ? e.message : String(e),
|
|
141
|
+
color: 'error',
|
|
142
|
+
})
|
|
143
|
+
} finally {
|
|
144
|
+
savingStorage.value = false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
36
148
|
async function saveSlack() {
|
|
37
149
|
if (!slack.clientId.trim() || !slack.clientSecret.trim() || !slack.redirectUrl.trim()) {
|
|
38
150
|
toast.add({ title: t('layout.accountDeployment.slack.validation'), color: 'error' })
|
|
@@ -275,5 +387,120 @@ async function clearWeb() {
|
|
|
275
387
|
</UButton>
|
|
276
388
|
</div>
|
|
277
389
|
</section>
|
|
390
|
+
|
|
391
|
+
<!-- Content storage (binary artifacts / screenshots) -->
|
|
392
|
+
<section v-if="storageCapability" class="space-y-2 border-t border-slate-800 pt-6">
|
|
393
|
+
<div class="flex items-center gap-2">
|
|
394
|
+
<h4 class="text-sm font-semibold text-slate-200">
|
|
395
|
+
{{ t('layout.accountDeployment.contentStorage.title') }}
|
|
396
|
+
</h4>
|
|
397
|
+
<UBadge
|
|
398
|
+
:color="
|
|
399
|
+
storageSummary?.backend && storageSummary.backend !== 'off' ? 'success' : 'neutral'
|
|
400
|
+
"
|
|
401
|
+
variant="subtle"
|
|
402
|
+
size="xs"
|
|
403
|
+
>
|
|
404
|
+
{{
|
|
405
|
+
storageSummary?.backend
|
|
406
|
+
? contentBackendLabels[storageSummary.backend]
|
|
407
|
+
: t('layout.accountDeployment.contentStorage.default', {
|
|
408
|
+
backend: contentBackendLabels[storageCapability.defaultBackend],
|
|
409
|
+
})
|
|
410
|
+
}}
|
|
411
|
+
</UBadge>
|
|
412
|
+
</div>
|
|
413
|
+
<p class="text-[11px] text-slate-400">
|
|
414
|
+
{{ t('layout.accountDeployment.contentStorage.description') }}
|
|
415
|
+
</p>
|
|
416
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
417
|
+
<USelect v-model="csBackend" :items="backendItems" value-key="value" size="sm" />
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<!-- Filesystem -->
|
|
421
|
+
<div v-if="csBackend === 'fs'" class="grid grid-cols-1 gap-2">
|
|
422
|
+
<UInput
|
|
423
|
+
v-model="cs.basePath"
|
|
424
|
+
:placeholder="t('layout.accountDeployment.contentStorage.basePath')"
|
|
425
|
+
size="sm"
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<!-- S3 / S3-compatible -->
|
|
430
|
+
<template v-if="csBackend === 's3'">
|
|
431
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
432
|
+
<UInput
|
|
433
|
+
v-model="cs.region"
|
|
434
|
+
:placeholder="t('layout.accountDeployment.contentStorage.region')"
|
|
435
|
+
size="sm"
|
|
436
|
+
/>
|
|
437
|
+
<UInput
|
|
438
|
+
v-model="cs.bucket"
|
|
439
|
+
:placeholder="t('layout.accountDeployment.contentStorage.bucket')"
|
|
440
|
+
size="sm"
|
|
441
|
+
/>
|
|
442
|
+
<UInput
|
|
443
|
+
v-model="cs.prefix"
|
|
444
|
+
:placeholder="t('layout.accountDeployment.contentStorage.prefix')"
|
|
445
|
+
size="sm"
|
|
446
|
+
/>
|
|
447
|
+
<UInput
|
|
448
|
+
v-model="cs.endpoint"
|
|
449
|
+
:placeholder="t('layout.accountDeployment.contentStorage.endpoint')"
|
|
450
|
+
size="sm"
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
<UCheckbox
|
|
454
|
+
v-model="cs.forcePathStyle"
|
|
455
|
+
:label="t('layout.accountDeployment.contentStorage.forcePathStyle')"
|
|
456
|
+
size="sm"
|
|
457
|
+
/>
|
|
458
|
+
<div class="flex items-center gap-2">
|
|
459
|
+
<span class="text-[11px] text-slate-400">
|
|
460
|
+
{{ t('layout.accountDeployment.contentStorage.accessKeys') }}
|
|
461
|
+
</span>
|
|
462
|
+
<UBadge
|
|
463
|
+
:color="storageSummary?.s3CredentialsConfigured ? 'success' : 'neutral'"
|
|
464
|
+
variant="subtle"
|
|
465
|
+
size="xs"
|
|
466
|
+
>
|
|
467
|
+
{{
|
|
468
|
+
storageSummary?.s3CredentialsConfigured
|
|
469
|
+
? t('layout.accountDeployment.configured')
|
|
470
|
+
: t('layout.accountDeployment.notSet')
|
|
471
|
+
}}
|
|
472
|
+
</UBadge>
|
|
473
|
+
</div>
|
|
474
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
475
|
+
<UInput
|
|
476
|
+
v-model="cs.accessKeyId"
|
|
477
|
+
type="password"
|
|
478
|
+
:placeholder="t('layout.accountDeployment.contentStorage.accessKeyId')"
|
|
479
|
+
size="sm"
|
|
480
|
+
/>
|
|
481
|
+
<UInput
|
|
482
|
+
v-model="cs.secretAccessKey"
|
|
483
|
+
type="password"
|
|
484
|
+
:placeholder="t('layout.accountDeployment.contentStorage.secretAccessKey')"
|
|
485
|
+
size="sm"
|
|
486
|
+
/>
|
|
487
|
+
</div>
|
|
488
|
+
<p class="text-[11px] text-slate-400">
|
|
489
|
+
{{ t('layout.accountDeployment.contentStorage.keysHint') }}
|
|
490
|
+
</p>
|
|
491
|
+
</template>
|
|
492
|
+
|
|
493
|
+
<div class="flex gap-2">
|
|
494
|
+
<UButton
|
|
495
|
+
color="primary"
|
|
496
|
+
size="xs"
|
|
497
|
+
icon="i-lucide-save"
|
|
498
|
+
:loading="savingStorage"
|
|
499
|
+
@click="saveStorage"
|
|
500
|
+
>
|
|
501
|
+
{{ t('common.save') }}
|
|
502
|
+
</UButton>
|
|
503
|
+
</div>
|
|
504
|
+
</section>
|
|
278
505
|
</div>
|
|
279
506
|
</template>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// fixed (or all its models become available).
|
|
9
9
|
import { computed } from 'vue'
|
|
10
10
|
|
|
11
|
+
const { t } = useI18n()
|
|
11
12
|
const ui = useUiStore()
|
|
12
13
|
const models = useModelsStore()
|
|
13
14
|
const modelPresets = useModelPresetsStore()
|
|
@@ -18,7 +19,9 @@ const open = computed({
|
|
|
18
19
|
set: (v: boolean) => (v ? ui.openAiPresetMismatch() : ui.closeAiPresetMismatch()),
|
|
19
20
|
})
|
|
20
21
|
|
|
21
|
-
const presetName = computed(
|
|
22
|
+
const presetName = computed(
|
|
23
|
+
() => modelPresets.defaultPreset?.name ?? t('providers.presetMismatch.defaultPresetName'),
|
|
24
|
+
)
|
|
22
25
|
|
|
23
26
|
/** Readable labels for the unavailable model ids (catalog label, else the raw id). */
|
|
24
27
|
const unavailableLabels = computed(() =>
|
|
@@ -32,19 +35,27 @@ function go(action: () => void) {
|
|
|
32
35
|
</script>
|
|
33
36
|
|
|
34
37
|
<template>
|
|
35
|
-
<UModal
|
|
38
|
+
<UModal
|
|
39
|
+
v-model:open="open"
|
|
40
|
+
:title="t('providers.presetMismatch.title')"
|
|
41
|
+
:ui="{ content: 'max-w-xl' }"
|
|
42
|
+
>
|
|
36
43
|
<template #body>
|
|
37
44
|
<div class="space-y-5">
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
<i18n-t
|
|
46
|
+
keypath="providers.presetMismatch.intro"
|
|
47
|
+
tag="p"
|
|
48
|
+
class="text-sm text-slate-300"
|
|
49
|
+
scope="global"
|
|
50
|
+
>
|
|
51
|
+
<template #name>
|
|
52
|
+
<span class="font-medium text-slate-100">{{ presetName }}</span>
|
|
53
|
+
</template>
|
|
54
|
+
</i18n-t>
|
|
44
55
|
|
|
45
56
|
<div class="rounded-lg border border-slate-700 bg-slate-900/50 p-3">
|
|
46
57
|
<p class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
47
|
-
|
|
58
|
+
{{ t('providers.presetMismatch.unavailable') }}
|
|
48
59
|
</p>
|
|
49
60
|
<div class="flex flex-wrap gap-1.5">
|
|
50
61
|
<UBadge
|
|
@@ -60,11 +71,13 @@ function go(action: () => void) {
|
|
|
60
71
|
</div>
|
|
61
72
|
|
|
62
73
|
<p class="text-[13px] text-slate-400">
|
|
63
|
-
|
|
74
|
+
{{ t('providers.presetMismatch.advice') }}
|
|
64
75
|
</p>
|
|
65
76
|
|
|
66
77
|
<div class="flex flex-wrap justify-end gap-2">
|
|
67
|
-
<UButton color="neutral" variant="ghost" size="sm" @click="open = false">
|
|
78
|
+
<UButton color="neutral" variant="ghost" size="sm" @click="open = false">
|
|
79
|
+
{{ t('providers.presetMismatch.later') }}
|
|
80
|
+
</UButton>
|
|
68
81
|
<UButton
|
|
69
82
|
color="neutral"
|
|
70
83
|
variant="subtle"
|
|
@@ -72,10 +85,10 @@ function go(action: () => void) {
|
|
|
72
85
|
icon="i-lucide-key-round"
|
|
73
86
|
@click="go(ui.openVendorCredentials)"
|
|
74
87
|
>
|
|
75
|
-
|
|
88
|
+
{{ t('providers.presetMismatch.configureVendors') }}
|
|
76
89
|
</UButton>
|
|
77
90
|
<UButton color="primary" size="sm" icon="i-lucide-cpu" @click="go(ui.openModelConfig)">
|
|
78
|
-
|
|
91
|
+
{{ t('providers.presetMismatch.editPresets') }}
|
|
79
92
|
</UButton>
|
|
80
93
|
</div>
|
|
81
94
|
</div>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// the banner, disappears automatically the moment a usable source exists.
|
|
8
8
|
import { computed } from 'vue'
|
|
9
9
|
|
|
10
|
+
const { t } = useI18n()
|
|
10
11
|
const ui = useUiStore()
|
|
11
12
|
|
|
12
13
|
const open = computed({
|
|
@@ -32,30 +33,34 @@ interface Route {
|
|
|
32
33
|
const routes = computed<Route[]>(() => [
|
|
33
34
|
{
|
|
34
35
|
icon: 'i-lucide-key-round',
|
|
35
|
-
title: '
|
|
36
|
-
body: '
|
|
37
|
-
cta: '
|
|
36
|
+
title: t('providers.onboarding.routes.keys.title'),
|
|
37
|
+
body: t('providers.onboarding.routes.keys.body'),
|
|
38
|
+
cta: t('providers.onboarding.routes.keys.cta'),
|
|
38
39
|
onSelect: () => go(ui.openVendorCredentials),
|
|
39
40
|
},
|
|
40
41
|
{
|
|
41
42
|
icon: 'i-lucide-route',
|
|
42
|
-
title: '
|
|
43
|
-
body: '
|
|
44
|
-
cta: '
|
|
43
|
+
title: t('providers.onboarding.routes.openrouter.title'),
|
|
44
|
+
body: t('providers.onboarding.routes.openrouter.body'),
|
|
45
|
+
cta: t('providers.onboarding.routes.openrouter.cta'),
|
|
45
46
|
onSelect: () => go(ui.openOpenRouter),
|
|
46
47
|
},
|
|
47
48
|
{
|
|
48
49
|
icon: 'i-lucide-server',
|
|
49
|
-
title: '
|
|
50
|
-
body: '
|
|
51
|
-
cta: '
|
|
50
|
+
title: t('providers.onboarding.routes.local.title'),
|
|
51
|
+
body: t('providers.onboarding.routes.local.body'),
|
|
52
|
+
cta: t('providers.onboarding.routes.local.cta'),
|
|
52
53
|
onSelect: () => go(ui.openLocalModels),
|
|
53
54
|
},
|
|
54
55
|
])
|
|
55
56
|
</script>
|
|
56
57
|
|
|
57
58
|
<template>
|
|
58
|
-
<UModal
|
|
59
|
+
<UModal
|
|
60
|
+
v-model:open="open"
|
|
61
|
+
:title="t('providers.onboarding.title')"
|
|
62
|
+
:ui="{ content: 'max-w-2xl' }"
|
|
63
|
+
>
|
|
59
64
|
<template #body>
|
|
60
65
|
<div class="space-y-5">
|
|
61
66
|
<div
|
|
@@ -64,11 +69,10 @@ const routes = computed<Route[]>(() => [
|
|
|
64
69
|
<UIcon name="i-lucide-cpu" class="mt-0.5 h-6 w-6 shrink-0 text-amber-400" />
|
|
65
70
|
<div class="min-w-0 text-sm text-amber-100/90">
|
|
66
71
|
<p class="font-medium text-amber-100">
|
|
67
|
-
|
|
72
|
+
{{ t('providers.onboarding.noModelTitle') }}
|
|
68
73
|
</p>
|
|
69
74
|
<p class="mt-1">
|
|
70
|
-
|
|
71
|
-
with Workers AI enabled — otherwise connect at least one source below.
|
|
75
|
+
{{ t('providers.onboarding.noModelBody') }}
|
|
72
76
|
</p>
|
|
73
77
|
</div>
|
|
74
78
|
</div>
|
|
@@ -97,8 +101,7 @@ const routes = computed<Route[]>(() => [
|
|
|
97
101
|
</div>
|
|
98
102
|
|
|
99
103
|
<p class="text-[11px] leading-relaxed text-slate-500">
|
|
100
|
-
|
|
101
|
-
environment configuration, not from this screen.
|
|
104
|
+
{{ t('providers.onboarding.operatorNote') }}
|
|
102
105
|
</p>
|
|
103
106
|
</div>
|
|
104
107
|
</template>
|