astrbbbb / dashboard /src /views /WelcomePage.vue
qa1145's picture
Upload 1245 files
8ede856 verified
<template>
<div class="welcome-page">
<v-container fluid class="pa-0">
<v-row class="px-4 py-3 pb-6">
<v-col cols="12">
<h1 class="text-h1 font-weight-bold mb-2 d-flex align-center">
{{ greetingText }} {{ greetingEmoji }}
</h1>
<p class="text-subtitle-1 text-medium-emphasis mb-0">
{{ tm('subtitle') }}
</p>
</v-col>
</v-row>
<v-row class="px-4">
<v-col cols="12">
<v-card class="welcome-card pa-6" elevation="0" border>
<div class="mb-4 text-h3 font-weight-bold">
{{ tm('onboard.title') }}
</div>
<v-timeline align="start" side="end" density="compact" class="welcome-timeline" truncate-line="both">
<v-timeline-item :dot-color="platformStepState === 'completed' ? 'success' : 'primary'"
:icon="platformStepState === 'completed' ? 'mdi-check' : 'mdi-numeric-1'" fill-dot size="small">
<div class="pl-2">
<div class="text-h6 font-weight-bold mb-1">{{ tm('onboard.step1Title') }}</div>
<p class="text-body-2 text-medium-emphasis mb-3">{{ tm('onboard.step1Desc') }}</p>
<div class="d-flex align-center">
<v-btn color="primary" variant="flat" rounded="pill" class="px-6" :loading="loadingPlatformDialog"
@click="openPlatformDialog">
{{ tm('onboard.configure') }}
</v-btn>
<div v-if="platformStepState === 'completed'"
class="text-success d-flex align-center text-body-2 font-weight-medium ml-3">
{{ tm('onboard.completed') }}
</div>
</div>
</div>
</v-timeline-item>
<v-timeline-item :dot-color="providerStepState === 'completed' ? 'success' : 'primary'"
:icon="providerStepState === 'completed' ? 'mdi-check' : 'mdi-numeric-2'" fill-dot size="small">
<div class="pl-2">
<div class="text-h6 font-weight-bold mb-1"
:class="{ 'text-medium-emphasis': platformStepState !== 'completed' }">{{ tm('onboard.step2Title')
}}
</div>
<p class="text-body-2 text-medium-emphasis mb-3">{{ tm('onboard.step2Desc') }}</p>
<div class="d-flex align-center">
<v-btn color="primary" variant="flat" rounded="pill" class="px-6" @click="openProviderDialog">
{{ tm('onboard.configure') }}
</v-btn>
<div v-if="providerStepState === 'completed'"
class="text-success d-flex align-center text-body-2 font-weight-medium ml-3">
{{ tm('onboard.completed') }}
</div>
</div>
</div>
</v-timeline-item>
</v-timeline>
</v-card>
</v-col>
</v-row>
<v-row class="px-4 mt-4">
<v-col cols="12">
<v-card class="welcome-card pa-6" elevation="0" border>
<div class="mb-4 text-h3 font-weight-bold">
{{ tm('resources.title') }}
</div>
<v-row>
<v-col cols="12" sm="4">
<!-- GitHub Card -->
<v-card variant="outlined" class="h-100 pa-4 d-flex flex-column"
href="https://github.com/AstrBotDevs/AstrBot/" target="_blank">
<div class="d-flex align-center mb-3">
<v-icon size="32" class="mr-3">mdi-github</v-icon>
<span class="text-h6 font-weight-bold">GitHub</span>
</div>
<p class="text-body-2 text-medium-emphasis mb-0">
{{ tm('resources.githubDesc') }}
</p>
</v-card>
</v-col>
<v-col cols="12" sm="4">
<!-- Docs Card -->
<v-card variant="outlined" class="h-100 pa-4 d-flex flex-column" href="https://docs.astrbot.app"
target="_blank">
<div class="d-flex align-center mb-3">
<v-icon size="32" class="mr-3">mdi-book-open-variant</v-icon>
<span class="text-h6 font-weight-bold">{{ tm('resources.docsTitle') }}</span>
</div>
<p class="text-body-2 text-medium-emphasis mb-0">
{{ tm('resources.docsDesc') }}
</p>
</v-card>
</v-col>
<v-col cols="12" sm="4">
<!-- Afdian Card -->
<v-card variant="outlined" class="h-100 pa-4 d-flex flex-column"
href="https://afdian.com/a/astrbot_team" target="_blank">
<div class="d-flex align-center mb-3">
<v-icon size="32" class="mr-3">mdi-hand-heart</v-icon>
<span class="text-h6 font-weight-bold">{{ tm('resources.afdianTitle') }}</span>
</div>
<p class="text-body-2 text-medium-emphasis mb-0">
{{ tm('resources.afdianDesc') }}
</p>
</v-card>
</v-col>
</v-row>
</v-card>
</v-col>
</v-row>
<v-row v-if="showAnnouncement" class="px-4 mb-4">
<v-col cols="12">
<v-card class="welcome-card pa-6" elevation="0" border>
<div class="mb-4 text-h3 font-weight-bold">
{{ tm('announcement.title') }}
</div>
<MarkdownRender
:content="welcomeAnnouncement"
:typewriter="false"
class="welcome-announcement-markdown markdown-content"
/>
</v-card>
</v-col>
</v-row>
</v-container>
<AddNewPlatform v-model:show="showAddPlatformDialog" :metadata="platformMetadata" :config_data="platformConfigData"
@refresh-config="loadPlatformConfigBase" />
<ProviderConfigDialog v-model="showProviderDialog" />
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch, onMounted } from 'vue';
import axios from 'axios';
import AddNewPlatform from '@/components/platform/AddNewPlatform.vue';
import ProviderConfigDialog from '@/components/chat/ProviderConfigDialog.vue';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import { useToast } from '@/utils/toast';
import { MarkdownRender } from 'markstream-vue';
import 'markstream-vue/index.css';
import 'highlight.js/styles/github.css';
type StepState = 'pending' | 'completed' | 'skipped';
const { tm } = useModuleI18n('features/welcome');
const { locale } = useI18n();
const { success: showSuccess, error: showError } = useToast();
const showAddPlatformDialog = ref(false);
const showProviderDialog = ref(false);
const loadingPlatformDialog = ref(false);
const platformMetadata = ref<Record<string, any>>({});
const platformConfigData = ref<Record<string, any>>({});
const platformCountBeforeOpen = ref(0);
const providerCountBeforeOpen = ref(0);
const platformStepState = ref<StepState>('pending');
const providerStepState = ref<StepState>('pending');
const welcomeAnnouncementRaw = ref<unknown>(null);
function resolveWelcomeAnnouncement(raw: unknown, currentLocale: string) {
if (typeof raw === 'string') {
return raw.trim();
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
return '';
}
const localeMap = raw as Record<string, unknown>;
const normalized = currentLocale.replace('-', '_');
const preferredKeys =
normalized.startsWith('zh')
? [normalized, 'zh_CN', 'zh-CN', 'zh', 'en_US', 'en-US', 'en']
: [normalized, 'en_US', 'en-US', 'en', 'zh_CN', 'zh-CN', 'zh'];
for (const key of preferredKeys) {
const value = localeMap[key];
if (typeof value === 'string' && value.trim().length > 0) {
return value.trim();
}
}
return '';
}
const welcomeAnnouncement = computed(() =>
resolveWelcomeAnnouncement(welcomeAnnouncementRaw.value, locale.value)
);
const showAnnouncement = computed(() => welcomeAnnouncement.value.length > 0);
const springFestivalDates: Record<number, string> = {
2025: '01-29',
2026: '02-17',
2027: '02-06',
2028: '01-26',
2029: '02-13',
2030: '02-03'
}
function isSpringFestival() {
const now = new Date();
const year = now.getFullYear();
const dateStr = springFestivalDates[year];
if (!dateStr) return false;
const [month, day] = dateStr.split('-').map(Number);
const festivalDate = new Date(year, month - 1, day);
const start = new Date(festivalDate);
start.setDate(festivalDate.getDate() - 5);
const end = new Date(festivalDate);
end.setDate(festivalDate.getDate() + 5);
// start of day for comparison
const nowTime = now.setHours(0, 0, 0, 0);
const startTime = start.setHours(0, 0, 0, 0);
const endTime = end.setHours(0, 0, 0, 0);
return nowTime >= startTime && nowTime <= endTime;
}
function isExactSpringFestivalDay() {
const now = new Date();
const year = now.getFullYear();
const dateStr = springFestivalDates[year];
if (!dateStr) return false;
const [month, day] = dateStr.split('-').map(Number);
const festivalDate = new Date(year, month - 1, day);
const nowTime = new Date(now).setHours(0, 0, 0, 0);
const festivalTime = festivalDate.setHours(0, 0, 0, 0);
return nowTime === festivalTime;
}
const greetingEmoji = computed(() => {
if (isExactSpringFestivalDay()) {
return '🧨';
}
const hour = new Date().getHours();
if (hour >= 0 && hour < 5) {
return '😴';
}
return '😊';
});
const greetingText = computed(() => {
if (isSpringFestival()) {
return tm('greeting.newYear');
}
const hour = new Date().getHours();
if (hour < 12) return tm('greeting.morning');
if (hour < 18) return tm('greeting.afternoon');
return tm('greeting.evening');
});
async function loadPlatformConfigBase() {
const res = await axios.get('/api/config/get');
platformMetadata.value = res.data.data.metadata || {};
platformConfigData.value = res.data.data.config || {};
}
function getChatProvidersFromTemplatePayload(payload: any) {
const providers = payload?.providers || [];
const sources = payload?.provider_sources || [];
const sourceMap = new Map();
sources.forEach((s: any) => sourceMap.set(s.id, s.provider_type));
return providers.filter((provider: any) => {
if (provider.provider_type) {
return provider.provider_type === 'chat_completion';
}
if (provider.provider_source_id) {
const type = sourceMap.get(provider.provider_source_id);
if (type === 'chat_completion') return true;
}
return String(provider.type || '').includes('chat_completion');
});
}
async function fetchChatProviders() {
const response = await axios.get('/api/config/provider/template');
if (response.data.status !== 'ok') {
throw new Error(response.data.message || tm('onboard.providerLoadFailed'));
}
return getChatProvidersFromTemplatePayload(response.data.data);
}
function pickDefaultProviderId(providers: any[]) {
if (!providers.length) return '';
const enabledProvider = providers.find((provider) => provider.enable !== false);
return (enabledProvider || providers[0]).id || '';
}
async function syncDefaultConfigProviderIfNeeded() {
const providers = await fetchChatProviders();
if (!providers.length) return;
const targetProviderId = pickDefaultProviderId(providers);
if (!targetProviderId) return;
const configRes = await axios.get('/api/config/abconf', { params: { id: 'default' } });
const configData = configRes.data?.data?.config || {};
if (!configData.provider_settings) {
configData.provider_settings = {};
}
if (configData.provider_settings.default_provider_id === targetProviderId) return;
configData.provider_settings.default_provider_id = targetProviderId;
const updateRes = await axios.post('/api/config/astrbot/update', {
conf_id: 'default',
config: configData
});
if (updateRes.data.status !== 'ok') {
throw new Error(updateRes.data.message || tm('onboard.providerUpdateFailed'));
}
showSuccess(tm('onboard.providerDefaultUpdated', { id: targetProviderId }));
}
async function loadWelcomeAnnouncement() {
try {
const res = await axios.get('https://cloud.astrbot.app/api/v1/announcement');
welcomeAnnouncementRaw.value = res?.data?.data?.notice?.welcome_page ?? null;
} catch (e) {
welcomeAnnouncementRaw.value = null;
console.error(e);
}
}
onMounted(async () => {
await loadWelcomeAnnouncement();
try {
await loadPlatformConfigBase();
if ((platformConfigData.value.platform || []).length > 0) {
platformStepState.value = 'completed';
}
} catch (e) {
console.error(e);
}
try {
const providers = await fetchChatProviders();
if (providers.length > 0) {
providerStepState.value = 'completed';
}
} catch (e) {
console.error(e);
}
});
async function openPlatformDialog() {
loadingPlatformDialog.value = true;
try {
await loadPlatformConfigBase();
platformCountBeforeOpen.value = (platformConfigData.value.platform || []).length;
showAddPlatformDialog.value = true;
} catch (err: any) {
showError(err?.response?.data?.message || err?.message || tm('onboard.platformLoadFailed'));
} finally {
loadingPlatformDialog.value = false;
}
}
async function openProviderDialog() {
try {
const providers = await fetchChatProviders();
providerCountBeforeOpen.value = providers.length;
showProviderDialog.value = true;
} catch (err: any) {
showError(err?.response?.data?.message || err?.message || tm('onboard.providerLoadFailed'));
}
}
watch(showAddPlatformDialog, async (visible, wasVisible) => {
if (!wasVisible || visible) return;
try {
await loadPlatformConfigBase();
const newCount = (platformConfigData.value.platform || []).length;
if (newCount > platformCountBeforeOpen.value) {
platformStepState.value = 'completed';
}
} catch (err: any) {
showError(err?.response?.data?.message || err?.message || tm('onboard.platformLoadFailed'));
}
});
watch(showProviderDialog, async (visible, wasVisible) => {
if (!wasVisible || visible) return;
try {
const providers = await fetchChatProviders();
if (providers.length > providerCountBeforeOpen.value) {
providerStepState.value = 'completed';
await syncDefaultConfigProviderIfNeeded();
}
} catch (err: any) {
showError(err?.response?.data?.message || err?.message || tm('onboard.providerUpdateFailed'));
}
});
</script>
<style scoped>
.welcome-page {
height: 100%;
}
.welcome-card {
border-radius: 16px;
}
.welcome-announcement-markdown {
line-height: 1.7;
}
</style>