feat: complete onboarding flow
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import type { RegistrationMode } from '~~/drizzle/schema';
|
||||
|
||||
type InstanceSettings = {
|
||||
registration: RegistrationMode;
|
||||
registrationLocked: boolean;
|
||||
};
|
||||
|
||||
const REGISTRATION_OPTIONS: { value: RegistrationMode; label: string; description: string }[] = [
|
||||
{
|
||||
value: 'open',
|
||||
label: 'Open',
|
||||
description: 'Anyone who can reach this URL can create an account.',
|
||||
},
|
||||
{
|
||||
value: 'invite',
|
||||
label: 'Invite only',
|
||||
description: 'Admins create invite links. Good default for a shared instance.',
|
||||
},
|
||||
{
|
||||
value: 'closed',
|
||||
label: 'Closed',
|
||||
description: 'No sign-ups. Only you use this instance.',
|
||||
},
|
||||
];
|
||||
|
||||
// Tint of the chosen option. UnoCSS can't put an opacity on a CSS variable, so
|
||||
// "bg-[var(--color-accent)]/10" comes out as the solid accent colour.
|
||||
const SELECTED_STYLE = {
|
||||
backgroundColor: 'color-mix(in srgb, var(--color-accent) 10%, transparent)',
|
||||
};
|
||||
|
||||
definePageMeta({
|
||||
layout: 'auth',
|
||||
});
|
||||
|
||||
const { isAdmin } = useAuth();
|
||||
|
||||
// the rest of first-run setup belongs to the admin who just made the account
|
||||
if (!isAdmin.value) {
|
||||
await navigateTo('/');
|
||||
}
|
||||
|
||||
const { data: instance } = await useFetch<InstanceSettings>('/api/admin/instance', {
|
||||
key: 'admin_instance',
|
||||
});
|
||||
|
||||
const registration = ref<RegistrationMode>(instance.value?.registration ?? 'invite');
|
||||
const locked = computed(() => !!instance.value?.registrationLocked);
|
||||
|
||||
const saving = ref(false);
|
||||
const error = ref('');
|
||||
|
||||
const submit = async () => {
|
||||
if (saving.value) return;
|
||||
|
||||
// nothing to save when the environment decides, or nothing changed
|
||||
if (locked.value || instance.value?.registration === registration.value) {
|
||||
return navigateTo('/setup/provider');
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
error.value = '';
|
||||
|
||||
try {
|
||||
instance.value = await $fetch<InstanceSettings>('/api/admin/instance', {
|
||||
method: 'PUT',
|
||||
body: { registration: registration.value },
|
||||
});
|
||||
// the sign in pages read this to decide whether to offer registering
|
||||
clearNuxtData('instance');
|
||||
} catch (err) {
|
||||
console.error('Failed to save registration:', err);
|
||||
error.value = 'That could not be saved. Try again.';
|
||||
saving.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = false;
|
||||
|
||||
return navigateTo('/setup/provider');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex flex-col items-center justify-center w-full h-full p-4 overflow-hidden">
|
||||
<!-- Background decorative blobs -->
|
||||
<div
|
||||
class="absolute -top-24 -right-24 w-96 h-96 bg-[var(--color-accent)] opacity-10 blur-[100px] rounded-full pointer-events-none">
|
||||
</div>
|
||||
<div
|
||||
class="absolute -bottom-24 -left-24 w-96 h-96 bg-[var(--color-accent)] opacity-5 blur-[100px] rounded-full pointer-events-none">
|
||||
</div>
|
||||
|
||||
<div class="z-10 max-w-104 w-full flex flex-col gap-5">
|
||||
<div class="flex flex-col items-center gap-1 text-center">
|
||||
<h1 class="text-3xl font-bold tracking-tight">Veridian</h1>
|
||||
<p class="text-[var(--text-secondary)]">Set up this instance</p>
|
||||
</div>
|
||||
|
||||
<OnboardingSetupSteps :current="2"></OnboardingSetupSteps>
|
||||
|
||||
<form class="flex flex-col gap-4" @submit.prevent="submit">
|
||||
<fieldset class="flex flex-col gap-1.5" :disabled="saving || locked">
|
||||
<legend
|
||||
class="mb-1.5 ml-1 text-sm font-medium uppercase tracking-wider text-[var(--text-secondary)]">
|
||||
Who can register
|
||||
</legend>
|
||||
|
||||
<label v-for="option in REGISTRATION_OPTIONS" :key="option.value"
|
||||
class="flex items-start gap-3 px-3 py-2.5 rounded-lg border transition-colors duration-200"
|
||||
:style="registration === option.value ? SELECTED_STYLE : undefined" :class="[
|
||||
registration === option.value
|
||||
? 'border-[var(--color-accent)]'
|
||||
: 'border-[var(--color-border)]',
|
||||
locked ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer',
|
||||
]">
|
||||
<!-- a plain radio: the auth layout sizes every input as a text field -->
|
||||
<input v-model="registration" type="radio" name="registration" :value="option.value"
|
||||
class="!w-auto !p-0 mt-0.75 flex-shrink-0" style="accent-color: var(--color-accent)" />
|
||||
<span class="flex flex-col gap-0.5">
|
||||
<span class="font-medium">{{ option.label }}</span>
|
||||
<span class="text-[13px] text-[var(--text-secondary)]">
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<p v-if="locked" class="flex gap-2 text-xs leading-normal text-[var(--text-secondary)]">
|
||||
<span class="i-mynaui-lock text-4 mt-0.25 flex-shrink-0" aria-hidden="true"></span>
|
||||
<span>
|
||||
Registration is turned off by the <code class="font-mono">DISABLE_SIGNUP</code>
|
||||
environment variable. Remove it and restart the server to choose here.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p v-if="error" role="alert" class="text-sm text-red-400">{{ error }}</p>
|
||||
|
||||
<button type="submit" :disabled="saving"
|
||||
class="accent w-full h-11 flex items-center justify-center font-semibold">
|
||||
<span v-if="saving" class="i-svg-spinners-90-ring-with-bg text-xl"></span>
|
||||
<span v-else>Continue</span>
|
||||
</button>
|
||||
|
||||
<p class="text-xs text-center text-[var(--text-secondary)]">
|
||||
This can be changed later in Settings → Instance.
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'auth',
|
||||
});
|
||||
|
||||
const { isAdmin } = useAuth();
|
||||
|
||||
// the rest of first-run setup belongs to the admin who just made the account
|
||||
if (!isAdmin.value) {
|
||||
await navigateTo('/');
|
||||
}
|
||||
|
||||
const finish = () => navigateTo('/');
|
||||
const back = () => navigateTo('/setup/instance');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex flex-col items-center justify-center w-full h-full p-4 overflow-hidden">
|
||||
<!-- Background decorative blobs -->
|
||||
<div
|
||||
class="absolute -top-24 -right-24 w-96 h-96 bg-[var(--color-accent)] opacity-10 blur-[100px] rounded-full pointer-events-none">
|
||||
</div>
|
||||
<div
|
||||
class="absolute -bottom-24 -left-24 w-96 h-96 bg-[var(--color-accent)] opacity-5 blur-[100px] rounded-full pointer-events-none">
|
||||
</div>
|
||||
|
||||
<div class="z-10 max-w-104 w-full flex flex-col gap-5">
|
||||
<div class="flex flex-col items-center gap-1 text-center">
|
||||
<h1 class="text-3xl font-bold tracking-tight">Veridian</h1>
|
||||
<p class="text-[var(--text-secondary)]">Connect your first AI provider</p>
|
||||
</div>
|
||||
|
||||
<OnboardingSetupSteps :current="3"></OnboardingSetupSteps>
|
||||
|
||||
<OnboardingProviderForm submit-label="Fetch models and finish" :show-back="true" @done="finish"
|
||||
@back="back">
|
||||
<template #note>
|
||||
<p class="text-xs leading-normal text-[var(--text-secondary)]">
|
||||
Providers are per account. Other people on this instance add their own keys, and nobody,
|
||||
including admins, can read yours.
|
||||
</p>
|
||||
</template>
|
||||
</OnboardingProviderForm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user