refactor(stage-ui): steps of onboarding dialog (#1224)
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
export { default as OnboardingDialog } from './onboarding-dialog.vue'
|
||||
export { default as OnboardingScreen } from './onboarding.vue'
|
||||
export type { OnboardingStep } from './types'
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { OnboardingStep } from './types'
|
||||
|
||||
import { useMediaQuery, useResizeObserver, useScreenSafeArea } from '@vueuse/core'
|
||||
import { DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTitle, VisuallyHidden } from 'reka-ui'
|
||||
import { DrawerContent, DrawerHandle, DrawerOverlay, DrawerPortal, DrawerRoot } from 'vaul-vue'
|
||||
@@ -6,6 +8,10 @@ import { onMounted } from 'vue'
|
||||
|
||||
import Onboarding from './onboarding.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
extraSteps?: OnboardingStep[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'configured'): void
|
||||
(e: 'skipped'): void
|
||||
@@ -28,7 +34,7 @@ onMounted(() => screenSafeArea.update())
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>Onboarding</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
<Onboarding @configured="emit('configured')" @skipped="emit('skipped')" />
|
||||
<Onboarding :extra-steps="props.extraSteps" @configured="emit('configured')" @skipped="emit('skipped')" />
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
@@ -37,7 +43,7 @@ onMounted(() => screenSafeArea.update())
|
||||
<DrawerOverlay class="fixed inset-0" />
|
||||
<DrawerContent class="fixed bottom-0 left-0 right-0 z-1000 mt-20 h-full max-h-[96%] flex flex-col rounded-t-2xl bg-neutral-50 px-4 pt-4 outline-none backdrop-blur-md dark:bg-neutral-900/95" :style="{ paddingBottom: `${Math.max(Number.parseFloat(screenSafeArea.bottom.value.replace('px', '')), 24)}px` }">
|
||||
<DrawerHandle />
|
||||
<Onboarding @configured="emit('configured')" @skipped="emit('skipped')" />
|
||||
<Onboarding :extra-steps="props.extraSteps" @configured="emit('configured')" @skipped="emit('skipped')" />
|
||||
</DrawerContent>
|
||||
</DrawerPortal>
|
||||
</DrawerRoot>
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { ProviderMetadata } from '../../../../stores/providers'
|
||||
import type {
|
||||
OnboardingStep,
|
||||
OnboardingStepGuard,
|
||||
OnboardingStepNextHandler,
|
||||
OnboardingStepPrevHandler,
|
||||
ProviderConfigData,
|
||||
} from './types'
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, nextTick, provide, ref } from 'vue'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import StepModelSelection from './step-model-selection.vue'
|
||||
import StepProviderConfiguration from './step-provider-configuration.vue'
|
||||
@@ -9,17 +18,19 @@ import StepWelcome from './step-welcome.vue'
|
||||
|
||||
import { useConsciousnessStore } from '../../../../stores/modules/consciousness'
|
||||
import { useProvidersStore } from '../../../../stores/providers'
|
||||
import { OnboardingContextKey } from './utils'
|
||||
|
||||
interface Emits {
|
||||
(e: 'configured'): void
|
||||
(e: 'skipped'): void
|
||||
}
|
||||
|
||||
const { extraSteps = [] } = defineProps<{
|
||||
extraSteps?: OnboardingStep[]
|
||||
}>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const step = ref(1)
|
||||
const step = ref(0)
|
||||
const direction = ref<'next' | 'previous'>('next')
|
||||
const pendingProviderConfig = ref<ProviderConfigData | null>(null)
|
||||
|
||||
const providersStore = useProvidersStore()
|
||||
const { providers, allChatProvidersMetadata } = storeToRefs(providersStore)
|
||||
@@ -45,37 +56,20 @@ const selectedProvider = computed(() => {
|
||||
})
|
||||
|
||||
// Reset validation state when provider changes
|
||||
function selectProvider(provider: typeof popularProviders.value[0]) {
|
||||
function selectProvider(provider: ProviderMetadata) {
|
||||
selectedProviderId.value = provider.id
|
||||
}
|
||||
|
||||
function handlePreviousStep() {
|
||||
if (step.value > 1) {
|
||||
direction.value = 'previous'
|
||||
step.value--
|
||||
}
|
||||
const requestPreviousStep: OnboardingStepPrevHandler = () => {
|
||||
return navigatePrevious()
|
||||
}
|
||||
|
||||
async function handleNextStep(configData?: { apiKey: string, baseUrl: string, accountId: string }) {
|
||||
// Step 3: Provider configuration - validate and save before proceeding
|
||||
if (step.value === 3 && configData) {
|
||||
await saveProviderConfiguration(configData)
|
||||
direction.value = 'next'
|
||||
step.value++
|
||||
return
|
||||
}
|
||||
|
||||
// Other steps: just proceed
|
||||
if (step.value < 4) {
|
||||
direction.value = 'next'
|
||||
step.value++
|
||||
}
|
||||
else {
|
||||
handleSave()
|
||||
}
|
||||
const requestNextStep: OnboardingStepNextHandler = async (configData?: ProviderConfigData) => {
|
||||
pendingProviderConfig.value = configData ?? null
|
||||
await navigateNext()
|
||||
}
|
||||
|
||||
async function saveProviderConfiguration(data: { apiKey: string, baseUrl: string, accountId: string }) {
|
||||
async function saveProviderConfiguration(data: ProviderConfigData) {
|
||||
if (!selectedProvider.value)
|
||||
return
|
||||
|
||||
@@ -108,77 +102,188 @@ async function handleSave() {
|
||||
emit('configured')
|
||||
}
|
||||
|
||||
provide(OnboardingContextKey, {
|
||||
selectedProviderId,
|
||||
selectedProvider,
|
||||
selectProvider,
|
||||
popularProviders,
|
||||
handleNextStep,
|
||||
handlePreviousStep,
|
||||
handleSave,
|
||||
const allSteps = computed<OnboardingStep[]>(() => {
|
||||
const coreSteps: OnboardingStep[] = [
|
||||
{
|
||||
id: 'welcome',
|
||||
component: StepWelcome,
|
||||
},
|
||||
{
|
||||
id: 'provider-selection',
|
||||
component: StepProviderSelection,
|
||||
props: () => ({
|
||||
selectedProviderId: selectedProviderId.value,
|
||||
popularProviders: popularProviders.value,
|
||||
onSelectProvider: selectProvider,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'provider-configuration',
|
||||
component: StepProviderConfiguration,
|
||||
props: () => ({
|
||||
selectedProviderId: selectedProviderId.value,
|
||||
selectedProvider: selectedProvider.value,
|
||||
}),
|
||||
beforeNext: async () => {
|
||||
if (!pendingProviderConfig.value)
|
||||
return false
|
||||
|
||||
await saveProviderConfiguration(pendingProviderConfig.value)
|
||||
pendingProviderConfig.value = null
|
||||
return true
|
||||
},
|
||||
},
|
||||
...extraSteps.map(step => ({
|
||||
...step,
|
||||
props: () => ({
|
||||
...step.props?.(),
|
||||
}),
|
||||
})),
|
||||
{
|
||||
id: 'model-selection',
|
||||
component: StepModelSelection,
|
||||
},
|
||||
]
|
||||
|
||||
return coreSteps
|
||||
})
|
||||
|
||||
const currentStep = computed(() => allSteps.value[step.value] ?? null)
|
||||
const isLastStep = computed(() => step.value === allSteps.value.length - 1)
|
||||
const currentStepProps = computed(() => currentStep.value?.props?.() ?? {})
|
||||
|
||||
async function canPassGuard(guard?: OnboardingStepGuard) {
|
||||
if (!guard)
|
||||
return true
|
||||
|
||||
return await guard()
|
||||
}
|
||||
|
||||
async function navigateNext() {
|
||||
if (!currentStep.value)
|
||||
return
|
||||
|
||||
if (!(await canPassGuard(currentStep.value.beforeNext)))
|
||||
return
|
||||
|
||||
if (isLastStep.value) {
|
||||
await handleSave()
|
||||
return
|
||||
}
|
||||
|
||||
direction.value = 'next'
|
||||
step.value++
|
||||
}
|
||||
|
||||
async function navigatePrevious() {
|
||||
if (!currentStep.value || step.value <= 0)
|
||||
return
|
||||
|
||||
if (!(await canPassGuard(currentStep.value.beforePrev)))
|
||||
return
|
||||
|
||||
direction.value = 'previous'
|
||||
step.value--
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h-full w-full>
|
||||
<div class="onboarding-step-container" h-full w-full>
|
||||
<Transition :name="direction === 'next' ? 'slide-next' : 'slide-prev'" mode="out-in">
|
||||
<StepWelcome v-if="step === 1" :key="1" />
|
||||
<StepProviderSelection v-else-if="step === 2" :key="2" />
|
||||
<StepProviderConfiguration v-else-if="step === 3" :key="3" />
|
||||
<StepModelSelection v-else-if="step === 4" :key="4" />
|
||||
<component
|
||||
:is="currentStep.component"
|
||||
v-if="currentStep"
|
||||
:key="currentStep.id"
|
||||
v-bind="currentStepProps"
|
||||
:on-next="requestNextStep"
|
||||
:on-previous="requestPreviousStep"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.onboarding-step-container {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.slide-next-enter-active,
|
||||
.slide-next-leave-active {
|
||||
transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.slide-next-enter-from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-next-enter-to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.slide-next-leave-from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.slide-next-leave-to {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Slide Previous Animation */
|
||||
.slide-next-leave-active,
|
||||
.slide-prev-enter-active,
|
||||
.slide-prev-leave-active {
|
||||
transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.slide-prev-enter-from {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
.slide-next-enter-active {
|
||||
animation: onboarding-slide-next-in 0.2s ease-in-out both;
|
||||
}
|
||||
|
||||
.slide-prev-enter-to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
.slide-next-leave-active {
|
||||
animation: onboarding-slide-next-out 0.2s ease-in-out both;
|
||||
}
|
||||
|
||||
.slide-prev-leave-from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
.slide-prev-enter-active {
|
||||
animation: onboarding-slide-prev-in 0.2s ease-in-out both;
|
||||
}
|
||||
|
||||
.slide-prev-leave-to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
.slide-prev-leave-active {
|
||||
animation: onboarding-slide-prev-out 0.2s ease-in-out both;
|
||||
}
|
||||
|
||||
@keyframes onboarding-slide-next-in {
|
||||
from {
|
||||
transform: translateX(2rem);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes onboarding-slide-next-out {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(-2rem);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes onboarding-slide-prev-in {
|
||||
from {
|
||||
transform: translateX(-2rem);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes onboarding-slide-prev-out {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(2rem);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.slide-next-enter-active,
|
||||
.slide-next-leave-active,
|
||||
.slide-prev-enter-active,
|
||||
.slide-prev-leave-active {
|
||||
animation-duration: 1ms;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+8
-5
@@ -1,17 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { OnboardingStepNextHandler, OnboardingStepPrevHandler } from './types'
|
||||
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Alert from '../../../misc/alert.vue'
|
||||
|
||||
import { useConsciousnessStore } from '../../../../stores/modules/consciousness'
|
||||
import { RadioCardManySelect } from '../../../menu'
|
||||
import { OnboardingContextKey } from './utils'
|
||||
|
||||
const props = defineProps<{
|
||||
onNext: OnboardingStepNextHandler
|
||||
onPrevious: OnboardingStepPrevHandler
|
||||
}>()
|
||||
const { t } = useI18n()
|
||||
const context = inject(OnboardingContextKey)!
|
||||
|
||||
const consciousnessStore = useConsciousnessStore()
|
||||
const {
|
||||
@@ -25,7 +28,7 @@ const {
|
||||
<template>
|
||||
<div h-full flex flex-col gap-4>
|
||||
<div sticky top-0 z-100 flex flex-shrink-0 items-center gap-2>
|
||||
<button outline-none @click="context.handlePreviousStep">
|
||||
<button outline-none @click="props.onPrevious">
|
||||
<div i-solar:alt-arrow-left-line-duotone h-5 w-5 />
|
||||
</button>
|
||||
<h2 class="flex-1 text-center text-xl text-neutral-800 font-semibold md:text-left md:text-2xl dark:text-neutral-100">
|
||||
@@ -73,7 +76,7 @@ const {
|
||||
:disabled="!activeModel"
|
||||
:loading="isLoadingActiveProviderModels"
|
||||
:label="t('settings.dialogs.onboarding.saveAndContinue')"
|
||||
@click="context.handleSave"
|
||||
@click="props.onNext"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+33
-24
@@ -1,15 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { ProviderMetadata } from '../../../../stores/providers'
|
||||
import type { OnboardingStepNextHandler, OnboardingStepPrevHandler } from './types'
|
||||
|
||||
import { Button, Callout, FieldInput } from '@proj-airi/ui'
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useProvidersStore } from '../../../../stores/providers'
|
||||
import { Alert } from '../../../misc'
|
||||
import { ProviderAccountIdInput } from '../../../scenarios/providers'
|
||||
import { OnboardingContextKey } from './utils'
|
||||
|
||||
interface Props {
|
||||
selectedProviderId: string
|
||||
selectedProvider: ProviderMetadata | null
|
||||
onNext: OnboardingStepNextHandler
|
||||
onPrevious: OnboardingStepPrevHandler
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const { t } = useI18n()
|
||||
const context = inject(OnboardingContextKey)!
|
||||
const providersStore = useProvidersStore()
|
||||
|
||||
const apiKey = ref('')
|
||||
@@ -21,7 +30,7 @@ const validationError = ref<any>()
|
||||
|
||||
// Initialize form with default values when provider changes
|
||||
function initializeForm() {
|
||||
const provider = context.selectedProvider.value
|
||||
const provider = props.selectedProvider
|
||||
if (!provider)
|
||||
return
|
||||
|
||||
@@ -36,7 +45,7 @@ function initializeForm() {
|
||||
}
|
||||
|
||||
// Watch for provider changes
|
||||
watch(() => context.selectedProvider.value?.id, initializeForm)
|
||||
watch(() => props.selectedProvider?.id, initializeForm)
|
||||
|
||||
watch([apiKey, baseUrl, accountId], () => {
|
||||
if (validation.value === 'failed' || validation.value === 'succeed') {
|
||||
@@ -47,19 +56,19 @@ watch([apiKey, baseUrl, accountId], () => {
|
||||
|
||||
// Computed properties
|
||||
const needsApiKey = computed(() => {
|
||||
if (!context.selectedProvider.value)
|
||||
if (!props.selectedProvider)
|
||||
return false
|
||||
return context.selectedProvider.value.id !== 'ollama' && context.selectedProvider.value.id !== 'player2'
|
||||
return props.selectedProvider.id !== 'ollama' && props.selectedProvider.id !== 'player2'
|
||||
})
|
||||
|
||||
const needsBaseUrl = computed(() => {
|
||||
if (!context.selectedProvider.value)
|
||||
if (!props.selectedProvider)
|
||||
return false
|
||||
return context.selectedProvider.value.id !== 'cloudflare-workers-ai'
|
||||
return props.selectedProvider.id !== 'cloudflare-workers-ai'
|
||||
})
|
||||
|
||||
const canProceed = computed(() => {
|
||||
if (!context.selectedProviderId.value)
|
||||
if (!props.selectedProviderId)
|
||||
return false
|
||||
|
||||
if (needsApiKey.value && !apiKey.value.trim())
|
||||
@@ -75,7 +84,7 @@ const primaryActionLabel = computed(() => {
|
||||
})
|
||||
|
||||
async function validateConfiguration() {
|
||||
if (!context.selectedProvider.value)
|
||||
if (!props.selectedProvider)
|
||||
return
|
||||
|
||||
validation.value = 'pending'
|
||||
@@ -89,11 +98,11 @@ async function validateConfiguration() {
|
||||
config.apiKey = apiKey.value.trim()
|
||||
if (needsBaseUrl.value)
|
||||
config.baseUrl = baseUrl.value.trim()
|
||||
if (context.selectedProvider.value.id === 'cloudflare-workers-ai')
|
||||
if (props.selectedProvider.id === 'cloudflare-workers-ai')
|
||||
config.accountId = accountId.value.trim()
|
||||
|
||||
// Validate using provider's validator
|
||||
const metadata = providersStore.getProviderMetadata(context.selectedProvider.value.id)
|
||||
const metadata = providersStore.getProviderMetadata(props.selectedProvider.id)
|
||||
const validationResult = await metadata.validators.validateProviderConfig(config)
|
||||
validation.value = validationResult.valid ? 'succeed' : 'failed'
|
||||
if (validation.value === 'failed') {
|
||||
@@ -111,7 +120,7 @@ async function validateConfiguration() {
|
||||
async function handleNext() {
|
||||
await validateConfiguration()
|
||||
if (validation.value === 'succeed') {
|
||||
await context.handleNextStep({
|
||||
await props.onNext({
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value,
|
||||
accountId: accountId.value,
|
||||
@@ -120,15 +129,15 @@ async function handleNext() {
|
||||
}
|
||||
|
||||
async function handleContinueAnyway() {
|
||||
if (!context.selectedProvider.value)
|
||||
if (!props.selectedProvider)
|
||||
return
|
||||
|
||||
await context.handleNextStep({
|
||||
await props.onNext({
|
||||
apiKey: apiKey.value,
|
||||
baseUrl: baseUrl.value,
|
||||
accountId: accountId.value,
|
||||
})
|
||||
providersStore.forceProviderConfigured(context.selectedProvider.value.id)
|
||||
providersStore.forceProviderConfigured(props.selectedProvider.id)
|
||||
}
|
||||
|
||||
// Placeholder helpers
|
||||
@@ -154,7 +163,7 @@ function getApiKeyPlaceholder(providerId: string): string {
|
||||
}
|
||||
|
||||
function getBaseUrlPlaceholder(_providerId: string): string {
|
||||
const defaultOptions = context.selectedProvider.value?.defaultOptions?.() || {}
|
||||
const defaultOptions = props.selectedProvider?.defaultOptions?.() || {}
|
||||
return (defaultOptions as any)?.baseUrl || 'https://api.example.com/v1/'
|
||||
}
|
||||
|
||||
@@ -165,15 +174,15 @@ initializeForm()
|
||||
<template>
|
||||
<div h-full flex flex-col gap-4>
|
||||
<div sticky top-0 z-100 flex flex-shrink-0 items-center gap-2>
|
||||
<button outline-none @click="context.handlePreviousStep">
|
||||
<button outline-none @click="props.onPrevious">
|
||||
<div i-solar:alt-arrow-left-line-duotone h-5 w-5 />
|
||||
</button>
|
||||
<h2 class="flex-1 text-center text-xl text-neutral-800 font-semibold md:text-left md:text-2xl dark:text-neutral-100">
|
||||
{{ t('settings.dialogs.onboarding.configureProvider', { provider: context.selectedProvider.value?.localizedName }) }}
|
||||
{{ t('settings.dialogs.onboarding.configureProvider', { provider: props.selectedProvider?.localizedName }) }}
|
||||
</h2>
|
||||
<div h-5 w-5 />
|
||||
</div>
|
||||
<div v-if="context.selectedProvider.value" flex-1 overflow-y-auto space-y-4>
|
||||
<div v-if="props.selectedProvider" flex-1 overflow-y-auto space-y-4>
|
||||
<Callout label="Keep your API keys and credentials safe!" theme="violet">
|
||||
<div>
|
||||
<div>
|
||||
@@ -191,7 +200,7 @@ initializeForm()
|
||||
<div v-if="needsApiKey">
|
||||
<FieldInput
|
||||
v-model="apiKey"
|
||||
:placeholder="getApiKeyPlaceholder(context.selectedProvider.value.id)"
|
||||
:placeholder="getApiKeyPlaceholder(props.selectedProvider.id)"
|
||||
type="password"
|
||||
label="API Key"
|
||||
description="Enter your API key for the selected provider."
|
||||
@@ -203,7 +212,7 @@ initializeForm()
|
||||
<div v-if="needsBaseUrl">
|
||||
<FieldInput
|
||||
v-model="baseUrl"
|
||||
:placeholder="getBaseUrlPlaceholder(context.selectedProvider.value.id)"
|
||||
:placeholder="getBaseUrlPlaceholder(props.selectedProvider.id)"
|
||||
type="text"
|
||||
label="Base URL"
|
||||
description="Enter the base URL for the provider's API."
|
||||
@@ -211,7 +220,7 @@ initializeForm()
|
||||
</div>
|
||||
|
||||
<!-- Account ID for Cloudflare -->
|
||||
<div v-if="context.selectedProvider.value.id === 'cloudflare-workers-ai'">
|
||||
<div v-if="props.selectedProvider.id === 'cloudflare-workers-ai'">
|
||||
<ProviderAccountIdInput v-model="accountId" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+28
-9
@@ -1,19 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import type { ProviderMetadata } from '../../../../stores/providers'
|
||||
import type { OnboardingStepNextHandler, OnboardingStepPrevHandler } from './types'
|
||||
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { inject } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { RadioCardDetail } from '../../../menu'
|
||||
import { OnboardingContextKey } from './utils'
|
||||
|
||||
interface Props {
|
||||
popularProviders: ProviderMetadata[]
|
||||
selectedProviderId: string
|
||||
onSelectProvider: (provider: ProviderMetadata) => void
|
||||
onNext: OnboardingStepNextHandler
|
||||
onPrevious: OnboardingStepPrevHandler
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const { t } = useI18n()
|
||||
const context = inject(OnboardingContextKey)!
|
||||
|
||||
const selectedProviderIdModel = computed({
|
||||
get: () => props.selectedProviderId,
|
||||
set: (providerId: string) => {
|
||||
const provider = props.popularProviders.find(item => item.id === providerId)
|
||||
if (provider)
|
||||
props.onSelectProvider(provider)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div h-full flex flex-col gap-4>
|
||||
<div sticky top-0 z-100 flex flex-shrink-0 items-center gap-2>
|
||||
<button outline-none @click="context.handlePreviousStep">
|
||||
<button outline-none @click="props.onPrevious">
|
||||
<div class="i-solar:alt-arrow-left-line-duotone h-5 w-5" />
|
||||
</button>
|
||||
<h2 class="flex-1 text-center text-xl text-neutral-800 font-semibold md:text-left md:text-2xl dark:text-neutral-100">
|
||||
@@ -24,22 +43,22 @@ const context = inject(OnboardingContextKey)!
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<RadioCardDetail
|
||||
v-for="provider in context.popularProviders.value"
|
||||
v-for="provider in props.popularProviders"
|
||||
:id="provider.id"
|
||||
:key="provider.id"
|
||||
v-model="context.selectedProviderId.value"
|
||||
v-model="selectedProviderIdModel"
|
||||
name="provider-selection"
|
||||
:value="provider.id"
|
||||
:title="provider.localizedName || provider.id"
|
||||
:description="provider.localizedDescription || ''"
|
||||
@click="context.selectProvider(provider)"
|
||||
@click="props.onSelectProvider(provider)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
:label="t('settings.dialogs.onboarding.next')"
|
||||
:disabled="!context.selectedProviderId.value"
|
||||
@click="context.handleNextStep"
|
||||
:disabled="!selectedProviderIdModel"
|
||||
@click="props.onNext"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { OnboardingStepNextHandler } from './types'
|
||||
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import onboardingLogo from '../../../../assets/onboarding.avif'
|
||||
|
||||
import { OnboardingContextKey } from './utils'
|
||||
interface Props {
|
||||
onNext: OnboardingStepNextHandler
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const { t } = useI18n()
|
||||
const context = inject(OnboardingContextKey)!
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -17,7 +20,7 @@ const context = inject(OnboardingContextKey)!
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ opacity: 0, scale: 0.5 }"
|
||||
:visible="{ opacity: 1, scale: 1 }"
|
||||
:enter="{ opacity: 1, scale: 1 }"
|
||||
:duration="500"
|
||||
class="mb-1 flex justify-center md:mb-4 lg:pt-16 md:pt-8"
|
||||
>
|
||||
@@ -26,7 +29,7 @@ const context = inject(OnboardingContextKey)!
|
||||
<h2
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:visible="{ opacity: 1, y: 0 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="500"
|
||||
class="mb-0 text-3xl text-neutral-800 font-bold md:mb-2 dark:text-neutral-100"
|
||||
>
|
||||
@@ -35,7 +38,7 @@ const context = inject(OnboardingContextKey)!
|
||||
<p
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:visible="{ opacity: 1, y: 0 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="500"
|
||||
:delay="100"
|
||||
class="text-sm text-neutral-600 md:text-lg dark:text-neutral-400"
|
||||
@@ -46,11 +49,11 @@ const context = inject(OnboardingContextKey)!
|
||||
<Button
|
||||
v-motion
|
||||
:initial="{ opacity: 0 }"
|
||||
:visible="{ opacity: 1 }"
|
||||
:enter="{ opacity: 1 }"
|
||||
:duration="500"
|
||||
:delay="200"
|
||||
:label="t('settings.dialogs.onboarding.start')"
|
||||
@click="context.handleNextStep"
|
||||
@click="props.onNext"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Component } from 'vue'
|
||||
|
||||
export type OnboardingStepGuard = () => Promise<boolean>
|
||||
export type OnboardingStepPrevHandler = () => Promise<void> | void
|
||||
|
||||
export interface ProviderConfigData {
|
||||
apiKey: string
|
||||
baseUrl: string
|
||||
accountId: string
|
||||
}
|
||||
|
||||
export type OnboardingStepNextHandler = (configData?: ProviderConfigData) => Promise<void> | void
|
||||
|
||||
export interface OnboardingStep {
|
||||
id: string
|
||||
component: Component<{
|
||||
configData?: ProviderConfigData
|
||||
onNext: OnboardingStepNextHandler
|
||||
onPrevious?: OnboardingStepPrevHandler
|
||||
}>
|
||||
props?: () => Record<string, unknown>
|
||||
beforeNext?: OnboardingStepGuard
|
||||
beforePrev?: OnboardingStepGuard
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
import type { ProviderMetadata } from '../../../../stores/providers'
|
||||
|
||||
export interface OnboardingContext {
|
||||
selectedProviderId: Ref<string>
|
||||
selectedProvider: Ref<ProviderMetadata | null>
|
||||
popularProviders: Ref<ProviderMetadata[]>
|
||||
selectProvider: (provider: ProviderMetadata) => void
|
||||
handleNextStep: (configData?: { apiKey: string, baseUrl: string, accountId: string }) => Promise<void>
|
||||
handlePreviousStep: () => void
|
||||
handleSave: () => void
|
||||
}
|
||||
|
||||
export const OnboardingContextKey: InjectionKey<OnboardingContext> = Symbol('onboarding-context')
|
||||
Reference in New Issue
Block a user