feat(stage-web): launch setup (#259)
This commit is contained in:
Vendored
+4
@@ -56,6 +56,10 @@
|
|||||||
],
|
],
|
||||||
|
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"typescript.preferences.includePackageJsonAutoImports": "off",
|
||||||
|
"typescript.suggest.autoImports": false,
|
||||||
|
"vue.server.hybridMode": true,
|
||||||
|
"vue.server.maxFileSize": 20971520,
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"appimage",
|
"appimage",
|
||||||
"localforage"
|
"localforage"
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AiriTamagotchiEvents } from './composables/tauri'
|
import type { AiriTamagotchiEvents } from './composables/tauri'
|
||||||
|
|
||||||
import { useMcpStore, useSettings } from '@proj-airi/stage-ui/stores'
|
import { FirstTimeSetupDialog } from '@proj-airi/stage-ui/components'
|
||||||
|
import { useFirstTimeSetupStore, useMcpStore, useSettings } from '@proj-airi/stage-ui/stores'
|
||||||
import { Window } from '@tauri-apps/api/window'
|
import { Window } from '@tauri-apps/api/window'
|
||||||
import { useEventListener } from '@vueuse/core'
|
import { useEventListener } from '@vueuse/core'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
@@ -17,6 +18,8 @@ const { language, themeColorsHue, themeColorsHueDynamic, allowVisibleOnAllWorksp
|
|||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const windowControlStore = useWindowControlStore()
|
const windowControlStore = useWindowControlStore()
|
||||||
const mcpStore = useMcpStore()
|
const mcpStore = useMcpStore()
|
||||||
|
const firstTimeSetupStore = useFirstTimeSetupStore()
|
||||||
|
const { shouldShowSetup } = storeToRefs(firstTimeSetupStore)
|
||||||
const { platform } = useAppRuntime()
|
const { platform } = useAppRuntime()
|
||||||
const { listen } = useTauriEvent<AiriTamagotchiEvents>()
|
const { listen } = useTauriEvent<AiriTamagotchiEvents>()
|
||||||
|
|
||||||
@@ -31,6 +34,8 @@ watch(language, () => {
|
|||||||
|
|
||||||
// FIXME: store settings to file
|
// FIXME: store settings to file
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// Initialize first-time setup check
|
||||||
|
firstTimeSetupStore.initializeSetupCheck()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(themeColorsHue, () => {
|
watch(themeColorsHue, () => {
|
||||||
@@ -47,6 +52,15 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Handle first-time setup events
|
||||||
|
function handleSetupConfigured() {
|
||||||
|
firstTimeSetupStore.markSetupCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSetupSkipped() {
|
||||||
|
firstTimeSetupStore.markSetupSkipped()
|
||||||
|
}
|
||||||
|
|
||||||
const isMac = computed(() => platform.value === 'macos')
|
const isMac = computed(() => platform.value === 'macos')
|
||||||
|
|
||||||
if (isMac.value) {
|
if (isMac.value) {
|
||||||
@@ -64,6 +78,13 @@ if (isMac.value) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
|
|
||||||
|
<!-- First Time Setup Dialog -->
|
||||||
|
<FirstTimeSetupDialog
|
||||||
|
v-model="shouldShowSetup"
|
||||||
|
@configured="handleSetupConfigured"
|
||||||
|
@skipped="handleSetupSkipped"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AiriTamagotchiEvents, Point } from '../composables/tauri'
|
import type { AiriTamagotchiEvents, Point } from '../composables/tauri'
|
||||||
|
|
||||||
import { WidgetStage } from '@proj-airi/stage-ui/components/scenes'
|
import { WidgetStage } from '@proj-airi/stage-ui/components/Scenes'
|
||||||
import { useMcpStore } from '@proj-airi/stage-ui/stores'
|
import { useMcpStore } from '@proj-airi/stage-ui/stores'
|
||||||
import { connectServer } from '@proj-airi/tauri-plugin-mcp'
|
import { connectServer } from '@proj-airi/tauri-plugin-mcp'
|
||||||
import { useWindowSize } from '@vueuse/core'
|
import { useWindowSize } from '@vueuse/core'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Live2DCanvas, Live2DModel } from '@proj-airi/stage-ui/components/scenes'
|
import { Live2DCanvas, Live2DModel } from '@proj-airi/stage-ui/components/Scenes'
|
||||||
import { useElementBounding, useMouse } from '@vueuse/core'
|
import { useElementBounding, useMouse } from '@vueuse/core'
|
||||||
import { Vibrant } from 'node-vibrant/browser'
|
import { Vibrant } from 'node-vibrant/browser'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
import { FirstTimeSetupDialog } from '@proj-airi/stage-ui/components'
|
||||||
|
import { useFirstTimeSetupStore, useSettings } from '@proj-airi/stage-ui/stores'
|
||||||
import { StageTransitionGroup } from '@proj-airi/ui-transitions'
|
import { StageTransitionGroup } from '@proj-airi/ui-transitions'
|
||||||
import { useDark } from '@vueuse/core'
|
import { useDark } from '@vueuse/core'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { computed, watch } from 'vue'
|
import { computed, onMounted, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
|
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const settings = storeToRefs(useSettings())
|
const settings = storeToRefs(useSettings())
|
||||||
|
const firstTimeSetupStore = useFirstTimeSetupStore()
|
||||||
|
const { shouldShowSetup } = storeToRefs(firstTimeSetupStore)
|
||||||
const isDark = useDark()
|
const isDark = useDark()
|
||||||
|
|
||||||
const primaryColor = computed(() => {
|
const primaryColor = computed(() => {
|
||||||
@@ -44,6 +47,20 @@ watch(settings.themeColorsHue, () => {
|
|||||||
watch(settings.themeColorsHueDynamic, () => {
|
watch(settings.themeColorsHueDynamic, () => {
|
||||||
document.documentElement.classList.toggle('dynamic-hue', settings.themeColorsHueDynamic.value)
|
document.documentElement.classList.toggle('dynamic-hue', settings.themeColorsHueDynamic.value)
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// Initialize first-time setup check when app mounts
|
||||||
|
onMounted(() => {
|
||||||
|
firstTimeSetupStore.initializeSetupCheck()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle first-time setup events
|
||||||
|
function handleSetupConfigured() {
|
||||||
|
firstTimeSetupStore.markSetupCompleted()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSetupSkipped() {
|
||||||
|
firstTimeSetupStore.markSetupSkipped()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -58,6 +75,13 @@ watch(settings.themeColorsHueDynamic, () => {
|
|||||||
>
|
>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</StageTransitionGroup>
|
</StageTransitionGroup>
|
||||||
|
|
||||||
|
<!-- First Time Setup Dialog -->
|
||||||
|
<FirstTimeSetupDialog
|
||||||
|
v-model="shouldShowSetup"
|
||||||
|
@configured="handleSetupConfigured"
|
||||||
|
@skipped="handleSetupSkipped"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { WidgetStage } from '@proj-airi/stage-ui/components/scenes'
|
import { WidgetStage } from '@proj-airi/stage-ui/components/Scenes'
|
||||||
import { useDark, useMouse } from '@vueuse/core'
|
import { useDark, useMouse } from '@vueuse/core'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ words:
|
|||||||
- tamagotchi
|
- tamagotchi
|
||||||
- tauri
|
- tauri
|
||||||
- taze
|
- taze
|
||||||
|
- togetherapi
|
||||||
- tolist
|
- tolist
|
||||||
- tresjs
|
- tresjs
|
||||||
- tsdown
|
- tsdown
|
||||||
|
|||||||
@@ -6,6 +6,22 @@ animations:
|
|||||||
Some pages will have their own transitions, this will override the stage
|
Some pages will have their own transitions, this will override the stage
|
||||||
transitions
|
transitions
|
||||||
title: Use Page Specific Transitions
|
title: Use Page Specific Transitions
|
||||||
|
firstTimeSetup:
|
||||||
|
title: Welcome to AIRI!
|
||||||
|
description: Let's set up your first AI provider to get started.
|
||||||
|
selectProvider: Choose an AI Provider
|
||||||
|
configureProvider: Configure {provider}
|
||||||
|
apiKey: API Key
|
||||||
|
apiKeyHelp: Get your API key from {provider} and paste it here
|
||||||
|
baseUrl: Base URL
|
||||||
|
baseUrlHelp: API endpoint URL (use default if unsure)
|
||||||
|
accountId: Account ID
|
||||||
|
validating: Validating configuration...
|
||||||
|
validationSuccess: ✓ Configuration is valid!
|
||||||
|
validationFailed: ✗ Configuration validation failed
|
||||||
|
validationError: 'Validation error: {error}'
|
||||||
|
skipForNow: Skip for now
|
||||||
|
saveAndContinue: Save and Continue
|
||||||
language:
|
language:
|
||||||
chinese: 简体中文
|
chinese: 简体中文
|
||||||
zh-CN: 简体中文
|
zh-CN: 简体中文
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ animations:
|
|||||||
use-page-specific-transitions:
|
use-page-specific-transitions:
|
||||||
description: 某些页面会有自己的过场动画,这将覆盖舞台过场动画
|
description: 某些页面会有自己的过场动画,这将覆盖舞台过场动画
|
||||||
title: 是否使用页面特定过场动画
|
title: 是否使用页面特定过场动画
|
||||||
|
firstTimeSetup:
|
||||||
|
title: 欢迎来到 AIRI!
|
||||||
|
description: 让我们设置您的第一个服务来源来开始使用。
|
||||||
|
selectProvider: 选择服务来源
|
||||||
|
configureProvider: 配置 {provider}
|
||||||
|
apiKey: API 密钥
|
||||||
|
apiKeyHelp: 从 {provider} 获取您的 API 密钥并粘贴到这里
|
||||||
|
baseUrl: 基础 URL
|
||||||
|
baseUrlHelp: API 端点 URL(如果不确定请使用默认值)
|
||||||
|
accountId: 账户 ID
|
||||||
|
validating: 正在验证配置...
|
||||||
|
validationSuccess: ✓ 配置有效!
|
||||||
|
validationFailed: ✗ 配置验证失败
|
||||||
|
validationError: '验证错误:{error}'
|
||||||
|
skipForNow: 暂时跳过
|
||||||
|
saveAndContinue: 保存并继续
|
||||||
language:
|
language:
|
||||||
chinese: 简体中文
|
chinese: 简体中文
|
||||||
english: English
|
english: English
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Range } from './Range.vue'
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||||
|
import { Input } from '@proj-airi/ui'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import {
|
||||||
|
DialogContent,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogRoot,
|
||||||
|
DialogTitle,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { RadioCardDetail } from '../Menu'
|
||||||
|
import { Button } from '../Misc'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
(e: 'configured'): void
|
||||||
|
(e: 'skipped'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const providersStore = useProvidersStore()
|
||||||
|
const { providers, allChatProvidersMetadata } = storeToRefs(providersStore)
|
||||||
|
|
||||||
|
// Dialog state
|
||||||
|
const showDialog = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: boolean) => emit('update:modelValue', value),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Popular providers for first-time setup
|
||||||
|
const popularProviders = computed(() => {
|
||||||
|
const popular = ['openai', 'anthropic', 'google-generative-ai', 'openrouter-ai', 'ollama', 'deepseek']
|
||||||
|
return allChatProvidersMetadata.value
|
||||||
|
.filter(provider => popular.includes(provider.id))
|
||||||
|
.sort((a, b) => popular.indexOf(a.id) - popular.indexOf(b.id))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Selected provider and form data
|
||||||
|
const selectedProviderId = ref('')
|
||||||
|
const apiKey = ref('')
|
||||||
|
const baseUrl = ref('')
|
||||||
|
const accountId = ref('')
|
||||||
|
|
||||||
|
// Computed selected provider
|
||||||
|
const selectedProvider = computed(() => {
|
||||||
|
return allChatProvidersMetadata.value.find(p => p.id === selectedProviderId.value) || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validation state
|
||||||
|
const isValidating = ref(false)
|
||||||
|
const isValid = ref(false)
|
||||||
|
const validationMessage = ref('')
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
const needsApiKey = computed(() => {
|
||||||
|
if (!selectedProvider.value)
|
||||||
|
return false
|
||||||
|
return selectedProvider.value.id !== 'ollama' && selectedProvider.value.id !== 'player2-api'
|
||||||
|
})
|
||||||
|
|
||||||
|
const needsBaseUrl = computed(() => {
|
||||||
|
if (!selectedProvider.value)
|
||||||
|
return false
|
||||||
|
return selectedProvider.value.id !== 'cloudflare-workers-ai'
|
||||||
|
})
|
||||||
|
|
||||||
|
const canSave = computed(() => {
|
||||||
|
if (!selectedProvider.value)
|
||||||
|
return false
|
||||||
|
|
||||||
|
if (needsApiKey.value && !apiKey.value.trim())
|
||||||
|
return false
|
||||||
|
if (needsBaseUrl.value && !baseUrl.value.trim())
|
||||||
|
return false
|
||||||
|
if (selectedProvider.value.id === 'cloudflare-workers-ai' && !accountId.value.trim())
|
||||||
|
return false
|
||||||
|
|
||||||
|
return isValid.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Provider selection
|
||||||
|
function selectProvider(provider: typeof popularProviders.value[0]) {
|
||||||
|
selectedProviderId.value = provider.id
|
||||||
|
|
||||||
|
// Set default values
|
||||||
|
const defaultOptions = provider.defaultOptions?.() || {}
|
||||||
|
baseUrl.value = (defaultOptions as any)?.baseUrl || ''
|
||||||
|
apiKey.value = ''
|
||||||
|
accountId.value = ''
|
||||||
|
|
||||||
|
// Reset validation
|
||||||
|
isValid.value = false
|
||||||
|
validationMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder helpers
|
||||||
|
function getApiKeyPlaceholder(_providerId: string): string {
|
||||||
|
const placeholders: Record<string, string> = {
|
||||||
|
'openai': 'sk-...',
|
||||||
|
'anthropic': 'sk-ant-...',
|
||||||
|
'google-generative-ai': 'GEMINI_API_KEY',
|
||||||
|
'openrouter-ai': 'sk-or-...',
|
||||||
|
'deepseek': 'sk-...',
|
||||||
|
'xai': 'xai-...',
|
||||||
|
'together-ai': 'togetherapi-...',
|
||||||
|
'mistral-ai': 'mis-...',
|
||||||
|
'moonshot-ai': 'ms-...',
|
||||||
|
'fireworks-ai': 'fw-...',
|
||||||
|
'featherless-ai': 'fw-...',
|
||||||
|
'novita-ai': 'nvt-...',
|
||||||
|
}
|
||||||
|
return placeholders[_providerId] || 'API Key'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseUrlPlaceholder(_providerId: string): string {
|
||||||
|
const defaultOptions = selectedProvider.value?.defaultOptions?.() || {}
|
||||||
|
return (defaultOptions as any)?.baseUrl || 'https://api.example.com/v1/'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
async function validateConfiguration() {
|
||||||
|
if (!selectedProvider.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
isValidating.value = true
|
||||||
|
validationMessage.value = t('settings.firstTimeSetup.validating')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prepare config object
|
||||||
|
const config: Record<string, unknown> = {}
|
||||||
|
|
||||||
|
if (needsApiKey.value)
|
||||||
|
config.apiKey = apiKey.value.trim()
|
||||||
|
if (needsBaseUrl.value)
|
||||||
|
config.baseUrl = baseUrl.value.trim()
|
||||||
|
if (selectedProvider.value.id === 'cloudflare-workers-ai')
|
||||||
|
config.accountId = accountId.value.trim()
|
||||||
|
|
||||||
|
// Validate using provider's validator
|
||||||
|
const metadata = providersStore.getProviderMetadata(selectedProvider.value.id)
|
||||||
|
isValid.value = await metadata.validators.validateProviderConfig(config)
|
||||||
|
|
||||||
|
if (isValid.value) {
|
||||||
|
validationMessage.value = t('settings.firstTimeSetup.validationSuccess')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
validationMessage.value = t('settings.firstTimeSetup.validationFailed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
isValid.value = false
|
||||||
|
validationMessage.value = t('validationError', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
isValidating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced validation function
|
||||||
|
const debouncedValidateConfiguration = useDebounceFn(() => {
|
||||||
|
if (!selectedProvider.value)
|
||||||
|
return
|
||||||
|
if (needsApiKey.value && !apiKey.value.trim())
|
||||||
|
return
|
||||||
|
if (needsBaseUrl.value && !baseUrl.value.trim())
|
||||||
|
return
|
||||||
|
if (selectedProvider.value.id === 'cloudflare-workers-ai' && !accountId.value.trim())
|
||||||
|
return
|
||||||
|
|
||||||
|
validateConfiguration()
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
// Watch for changes and validate
|
||||||
|
watch([apiKey, baseUrl, accountId], () => {
|
||||||
|
if (selectedProvider.value && (apiKey.value || baseUrl.value || accountId.value)) {
|
||||||
|
debouncedValidateConfiguration()
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
function handleSkip() {
|
||||||
|
showDialog.value = false
|
||||||
|
emit('skipped')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!selectedProvider.value || !canSave.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Save configuration to providers store
|
||||||
|
const config: Record<string, unknown> = {}
|
||||||
|
|
||||||
|
if (needsApiKey.value)
|
||||||
|
config.apiKey = apiKey.value.trim()
|
||||||
|
if (needsBaseUrl.value)
|
||||||
|
config.baseUrl = baseUrl.value.trim()
|
||||||
|
if (selectedProvider.value.id === 'cloudflare-workers-ai')
|
||||||
|
config.accountId = accountId.value.trim()
|
||||||
|
|
||||||
|
providers.value[selectedProvider.value.id] = {
|
||||||
|
...providers.value[selectedProvider.value.id],
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
|
||||||
|
showDialog.value = false
|
||||||
|
emit('configured')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize with first popular provider
|
||||||
|
onMounted(() => {
|
||||||
|
if (popularProviders.value.length > 0) {
|
||||||
|
selectedProviderId.value = popularProviders.value[0].id
|
||||||
|
selectProvider(popularProviders.value[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogRoot :open="showDialog" @update:open="value => showDialog = value">
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay class="data-[state=open]:animate-fadeIn data-[state=closed]:animate-fadeOut fixed inset-0 z-[9999] bg-black/50 backdrop-blur-sm" />
|
||||||
|
<DialogContent class="data-[state=open]:animate-contentShow data-[state=closed]:animate-contentHide fixed left-1/2 top-1/2 z-[9999] mx-4 max-w-2xl w-[92vw] transform rounded-lg bg-white p-8 shadow-xl -translate-x-1/2 -translate-y-1/2 dark:bg-neutral-900">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<div class="mb-4 flex justify-center">
|
||||||
|
<div class="rounded-full from-primary to-primary-600 bg-gradient-to-br p-4">
|
||||||
|
<div class="i-solar:ghost-smile-bold-duotone text-4xl text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogTitle class="mb-2 text-3xl text-neutral-800 font-bold dark:text-neutral-100">
|
||||||
|
{{ t('settings.firstTimeSetup.title') }}
|
||||||
|
</DialogTitle>
|
||||||
|
<p class="text-lg text-neutral-600 dark:text-neutral-400">
|
||||||
|
{{ t('settings.firstTimeSetup.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Provider Selection -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="mb-4 text-xl text-neutral-800 font-semibold dark:text-neutral-100">
|
||||||
|
{{ t('settings.firstTimeSetup.selectProvider') }}
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
|
<RadioCardDetail
|
||||||
|
v-for="provider in popularProviders"
|
||||||
|
:id="provider.id"
|
||||||
|
:key="provider.id"
|
||||||
|
v-model="selectedProviderId"
|
||||||
|
name="provider-selection"
|
||||||
|
:value="provider.id"
|
||||||
|
:title="provider.localizedName || provider.id"
|
||||||
|
:description="provider.localizedDescription || ''"
|
||||||
|
@click="selectProvider(provider)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuration Form -->
|
||||||
|
<div v-if="selectedProvider" class="mb-6">
|
||||||
|
<h3 class="mb-4 text-lg text-neutral-800 font-medium dark:text-neutral-100">
|
||||||
|
{{ t('settings.configureProvider', { provider: selectedProvider.localizedName }) }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- API Key Input -->
|
||||||
|
<div v-if="needsApiKey">
|
||||||
|
<label class="mb-2 block text-sm text-neutral-700 font-medium dark:text-neutral-300">
|
||||||
|
API Key
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
v-model="apiKey"
|
||||||
|
type="password"
|
||||||
|
:placeholder="getApiKeyPlaceholder(selectedProvider.id)"
|
||||||
|
class="w-full"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Base URL Input -->
|
||||||
|
<div v-if="needsBaseUrl">
|
||||||
|
<label class="mb-2 block text-sm text-neutral-700 font-medium dark:text-neutral-300">
|
||||||
|
Base URL
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
v-model="baseUrl"
|
||||||
|
type="url"
|
||||||
|
:placeholder="getBaseUrlPlaceholder(selectedProvider.id)"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Account ID for Cloudflare -->
|
||||||
|
<div v-if="selectedProvider.id === 'cloudflare-workers-ai'">
|
||||||
|
<label class="mb-2 block text-sm text-neutral-700 font-medium dark:text-neutral-300">
|
||||||
|
Account ID
|
||||||
|
<span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
v-model="accountId"
|
||||||
|
type="text"
|
||||||
|
placeholder="Account ID"
|
||||||
|
class="w-full"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Validation Status -->
|
||||||
|
<div v-if="validationMessage" class="mt-4">
|
||||||
|
<div
|
||||||
|
class="flex items-center rounded-lg p-3" :class="[
|
||||||
|
isValidating
|
||||||
|
? 'bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
||||||
|
: isValid
|
||||||
|
? 'bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-300'
|
||||||
|
: 'bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-300',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mr-2 text-lg" :class="[
|
||||||
|
isValidating
|
||||||
|
? 'i-svg-spinners:3-dots-fade'
|
||||||
|
: isValid
|
||||||
|
? 'i-mdi:check-circle'
|
||||||
|
: 'i-mdi:alert-circle',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
{{ validationMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
:label="t('settings.firstTimeSetup.skipForNow')"
|
||||||
|
@click="handleSkip"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
:disabled="!canSave"
|
||||||
|
:label="t('settings.firstTimeSetup.saveAndContinue')"
|
||||||
|
@click="handleSave"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogPortal>
|
||||||
|
</DialogRoot>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as FirstTimeSetupDialog } from './OnboardingDialog.vue'
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export * from './DataGui'
|
||||||
|
export * from './FirstTimeSetup'
|
||||||
export * from './Gadgets'
|
export * from './Gadgets'
|
||||||
export * from './Graphics'
|
export * from './Graphics'
|
||||||
export * from './Layouts'
|
export * from './Layouts'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './audio'
|
export * from './audio'
|
||||||
export * from './chat'
|
export * from './chat'
|
||||||
|
export * from './onboarding-setup'
|
||||||
export * from './llm'
|
export * from './llm'
|
||||||
export * from './mcp'
|
export * from './mcp'
|
||||||
export * from './modules'
|
export * from './modules'
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { useLocalStorage } from '@vueuse/core'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { computed, ref, nextTick } from 'vue'
|
||||||
|
|
||||||
|
import { useProvidersStore } from './providers'
|
||||||
|
|
||||||
|
export const useFirstTimeSetupStore = defineStore('firstTimeSetup', () => {
|
||||||
|
const providersStore = useProvidersStore()
|
||||||
|
|
||||||
|
// Track if first-time setup has been completed or skipped
|
||||||
|
const hasCompletedSetup = useLocalStorage('firstTimeSetup/completed', false)
|
||||||
|
const hasSkippedSetup = useLocalStorage('firstTimeSetup/skipped', false)
|
||||||
|
|
||||||
|
// Track if we should show the setup dialog
|
||||||
|
const shouldShowSetup = ref(false)
|
||||||
|
|
||||||
|
// Check if any essential provider is configured
|
||||||
|
const hasEssentialProviderConfigured = computed(() => {
|
||||||
|
const essentialProviders = ['openai', 'anthropic', 'google-generative-ai', 'openrouter-ai', 'ollama', 'deepseek']
|
||||||
|
return essentialProviders.some(providerId => providersStore.configuredProviders[providerId])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if first-time setup should be shown
|
||||||
|
const needsFirstTimeSetup = computed(() => {
|
||||||
|
// Don't show if already completed or skipped
|
||||||
|
if (hasCompletedSetup.value || hasSkippedSetup.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show if user already has essential providers configured
|
||||||
|
if (hasEssentialProviderConfigured.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize setup check
|
||||||
|
async function initializeSetupCheck() {
|
||||||
|
if (needsFirstTimeSetup.value) {
|
||||||
|
// Use nextTick to ensure the app is fully rendered before showing dialog
|
||||||
|
await nextTick()
|
||||||
|
shouldShowSetup.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark setup as completed
|
||||||
|
function markSetupCompleted() {
|
||||||
|
hasCompletedSetup.value = true
|
||||||
|
hasSkippedSetup.value = false
|
||||||
|
shouldShowSetup.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark setup as skipped
|
||||||
|
function markSetupSkipped() {
|
||||||
|
hasSkippedSetup.value = true
|
||||||
|
shouldShowSetup.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset setup state (for testing or re-showing setup)
|
||||||
|
function resetSetupState() {
|
||||||
|
hasCompletedSetup.value = false
|
||||||
|
hasSkippedSetup.value = false
|
||||||
|
shouldShowSetup.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force show setup dialog
|
||||||
|
function forceShowSetup() {
|
||||||
|
shouldShowSetup.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasCompletedSetup,
|
||||||
|
hasSkippedSetup,
|
||||||
|
shouldShowSetup,
|
||||||
|
hasEssentialProviderConfigured,
|
||||||
|
needsFirstTimeSetup,
|
||||||
|
initializeSetupCheck,
|
||||||
|
markSetupCompleted,
|
||||||
|
markSetupSkipped,
|
||||||
|
resetSetupState,
|
||||||
|
forceShowSetup,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -28,7 +28,8 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
"@vue-macros/volar/define-models",
|
"@vue-macros/volar/define-models",
|
||||||
"@vue-macros/volar/define-slots"
|
"@vue-macros/volar/define-slots"
|
||||||
]
|
],
|
||||||
|
"skipTemplateCodegen": false
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"env.d.ts",
|
"env.d.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user