1124 lines
28 KiB
Vue
1124 lines
28 KiB
Vue
<script setup lang="ts">
|
|
import {
|
|
ArrowRightOutlined,
|
|
CheckCircleOutlined,
|
|
GlobalOutlined,
|
|
PlusOutlined,
|
|
ShopOutlined,
|
|
TagsOutlined,
|
|
} from '@ant-design/icons-vue'
|
|
import { ApiClientError } from '@geo/http-client'
|
|
import type { Brand, BrandLibrarySummary, BrandRequest, QuestionCandidate } from '@geo/shared-types'
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
|
import { message } from 'ant-design-vue'
|
|
import { computed, onBeforeUnmount, reactive, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useRouter } from 'vue-router'
|
|
|
|
import AiWaitingModal from '@/components/AiWaitingModal.vue'
|
|
import { brandsApi } from '@/lib/api'
|
|
import {
|
|
formatError,
|
|
getQuestionMaterializeErrorMessage,
|
|
isDuplicateQuestionSkipReason,
|
|
} from '@/lib/errors'
|
|
import { useCompanyStore } from '@/stores/company'
|
|
|
|
const DEFAULT_SELECTED_QUESTION_COUNT = 5
|
|
|
|
interface CandidateRow extends QuestionCandidate {
|
|
id: string
|
|
selected: boolean
|
|
}
|
|
|
|
const router = useRouter()
|
|
const queryClient = useQueryClient()
|
|
const companyStore = useCompanyStore()
|
|
const { t } = useI18n()
|
|
|
|
const brandModalOpen = ref(false)
|
|
const questionModalOpen = ref(false)
|
|
const createdBrand = ref<Brand | null>(null)
|
|
const candidateRows = ref<CandidateRow[]>([])
|
|
const questionNotice = ref('')
|
|
const aiWaitingOpen = ref(false)
|
|
const aiProgress = ref(12)
|
|
const aiStageIndex = ref(0)
|
|
|
|
let aiProgressTimer: number | null = null
|
|
let aiCloseTimer: number | null = null
|
|
|
|
const brandForm = reactive({
|
|
name: '',
|
|
website: '',
|
|
description: '',
|
|
})
|
|
|
|
const brandLibrarySummaryQuery = useQuery({
|
|
queryKey: ['brands', 'library-summary'],
|
|
queryFn: () => brandsApi.getLibrarySummary(),
|
|
})
|
|
|
|
const brandLibrarySummary = computed<BrandLibrarySummary | null>(
|
|
() => brandLibrarySummaryQuery.data.value ?? null,
|
|
)
|
|
|
|
const brandLimitReached = computed(() => {
|
|
if (!brandLibrarySummary.value) {
|
|
return false
|
|
}
|
|
return brandLibrarySummary.value.remaining_brands <= 0
|
|
})
|
|
|
|
const selectableQuestionLimit = computed(() => {
|
|
const summary = brandLibrarySummary.value
|
|
if (!summary) {
|
|
return Number.POSITIVE_INFINITY
|
|
}
|
|
if (typeof summary.remaining_questions === 'number') {
|
|
return Math.max(0, summary.remaining_questions)
|
|
}
|
|
if (typeof summary.max_questions === 'number' && typeof summary.used_questions === 'number') {
|
|
return Math.max(0, summary.max_questions - summary.used_questions)
|
|
}
|
|
if (typeof summary.max_questions === 'number') {
|
|
return Math.max(0, summary.max_questions)
|
|
}
|
|
return Number.POSITIVE_INFINITY
|
|
})
|
|
|
|
const selectableQuestionLimitText = computed(() => {
|
|
if (Number.isFinite(selectableQuestionLimit.value)) {
|
|
return String(selectableQuestionLimit.value)
|
|
}
|
|
return '--'
|
|
})
|
|
|
|
const selectedCandidates = computed(() => candidateRows.value.filter((item) => item.selected))
|
|
|
|
const hasExistingBrands = computed(() => companyStore.brands.length > 0)
|
|
|
|
const createBrandMutation = useMutation({
|
|
mutationFn: (payload: BrandRequest) => brandsApi.create(payload),
|
|
})
|
|
|
|
const questionGenerationMutation = useMutation({
|
|
mutationFn: (payload: { brandId: number; seedTopic: string }) =>
|
|
brandsApi.distillQuestions(payload.brandId, {
|
|
seed_topic: payload.seedTopic,
|
|
}),
|
|
})
|
|
|
|
const materializeQuestionsMutation = useMutation({
|
|
mutationFn: (payload: { brandId: number; questions: string[] }) =>
|
|
brandsApi.materializeQuestions(payload.brandId, {
|
|
source: 'ai_distill',
|
|
questions: payload.questions.map((text) => ({ text })),
|
|
}),
|
|
})
|
|
|
|
const aiStages = computed(() => [
|
|
t('onboardingBrand.ai.stages.fetch'),
|
|
t('onboardingBrand.ai.stages.generate'),
|
|
t('onboardingBrand.ai.stages.refine'),
|
|
])
|
|
|
|
function openBrandModal(): void {
|
|
if (brandLimitReached.value) {
|
|
message.warning(
|
|
t('brands.messages.brandLimitReached', {
|
|
limit: brandLibrarySummary.value?.max_brands ?? 0,
|
|
}),
|
|
)
|
|
return
|
|
}
|
|
|
|
brandForm.name = ''
|
|
brandForm.website = ''
|
|
brandForm.description = ''
|
|
brandModalOpen.value = true
|
|
}
|
|
|
|
function clearAiTimers(): void {
|
|
if (aiProgressTimer !== null) {
|
|
window.clearInterval(aiProgressTimer)
|
|
aiProgressTimer = null
|
|
}
|
|
if (aiCloseTimer !== null) {
|
|
window.clearTimeout(aiCloseTimer)
|
|
aiCloseTimer = null
|
|
}
|
|
}
|
|
|
|
function beginAiTask(): void {
|
|
clearAiTimers()
|
|
aiWaitingOpen.value = true
|
|
aiProgress.value = 12
|
|
aiStageIndex.value = 0
|
|
aiProgressTimer = window.setInterval(() => {
|
|
const nextProgress = Math.min(92, aiProgress.value + 8)
|
|
aiProgress.value = nextProgress
|
|
aiStageIndex.value = nextProgress >= 66 ? 2 : nextProgress >= 38 ? 1 : 0
|
|
}, 700)
|
|
}
|
|
|
|
function finishAiTask(): void {
|
|
clearAiTimers()
|
|
aiProgress.value = 100
|
|
aiStageIndex.value = aiStages.value.length - 1
|
|
aiCloseTimer = window.setTimeout(() => {
|
|
aiWaitingOpen.value = false
|
|
aiCloseTimer = null
|
|
}, 180)
|
|
}
|
|
|
|
onBeforeUnmount(clearAiTimers)
|
|
|
|
function buildSeedTopic(brand: Brand): string {
|
|
return [
|
|
brand.name,
|
|
brand.website ?? brandForm.website.trim(),
|
|
brand.description ?? brandForm.description.trim(),
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
}
|
|
|
|
function normalizeQuestionText(value: string): string {
|
|
return value.trim().replace(/\s+/g, '')
|
|
}
|
|
|
|
function normalizeQuestionKey(value: string): string {
|
|
return normalizeQuestionText(value)
|
|
.replace(/[\p{P}]/gu, '')
|
|
.toLowerCase()
|
|
}
|
|
|
|
function toCandidateRows(candidates: QuestionCandidate[]): CandidateRow[] {
|
|
const rows: CandidateRow[] = []
|
|
const seen = new Set<string>()
|
|
|
|
for (const candidate of candidates) {
|
|
const text = normalizeQuestionText(candidate.text)
|
|
const key = normalizeQuestionKey(text)
|
|
if (!text || !key || seen.has(key)) {
|
|
continue
|
|
}
|
|
seen.add(key)
|
|
rows.push({
|
|
...candidate,
|
|
text,
|
|
id: `${Date.now()}-${rows.length}-${key}`,
|
|
selected: false,
|
|
})
|
|
}
|
|
|
|
return rows
|
|
}
|
|
|
|
function applyDefaultSelection(rows: CandidateRow[]): void {
|
|
const limit = Number.isFinite(selectableQuestionLimit.value)
|
|
? Math.min(DEFAULT_SELECTED_QUESTION_COUNT, selectableQuestionLimit.value)
|
|
: DEFAULT_SELECTED_QUESTION_COUNT
|
|
if (limit <= 0) {
|
|
return
|
|
}
|
|
|
|
let selected = 0
|
|
const preferredRows = rows.filter((row) => !row.suggest_skip && !row.too_short && !row.duplicate)
|
|
const orderedRows = [...preferredRows, ...rows.filter((row) => !preferredRows.includes(row))]
|
|
|
|
for (const row of orderedRows) {
|
|
if (selected >= limit) {
|
|
break
|
|
}
|
|
row.selected = true
|
|
selected += 1
|
|
}
|
|
}
|
|
|
|
async function refreshAppData(): Promise<void> {
|
|
await Promise.all([
|
|
queryClient.invalidateQueries({ queryKey: ['brands'] }),
|
|
queryClient.invalidateQueries({ queryKey: ['workspace'] }),
|
|
queryClient.invalidateQueries({ queryKey: ['articles'] }),
|
|
queryClient.invalidateQueries({ queryKey: ['tracking'] }),
|
|
queryClient.invalidateQueries({ queryKey: ['schedules'] }),
|
|
queryClient.invalidateQueries({ queryKey: ['instantTasks'] }),
|
|
])
|
|
}
|
|
|
|
async function enterWorkspace(brand: Brand | null = createdBrand.value): Promise<void> {
|
|
if (brand) {
|
|
companyStore.setCurrentBrand(brand.id)
|
|
}
|
|
await refreshAppData()
|
|
if (brand) {
|
|
await companyStore.refreshBrands()
|
|
companyStore.setCurrentBrand(brand.id)
|
|
}
|
|
await router.replace({ name: 'workspace' })
|
|
}
|
|
|
|
async function enterExistingBrand(brand: Brand): Promise<void> {
|
|
await enterWorkspace(brand)
|
|
}
|
|
|
|
function toggleCandidate(row: CandidateRow): void {
|
|
if (row.selected) {
|
|
row.selected = false
|
|
return
|
|
}
|
|
|
|
if (
|
|
Number.isFinite(selectableQuestionLimit.value) &&
|
|
selectedCandidates.value.length >= selectableQuestionLimit.value
|
|
) {
|
|
message.warning(
|
|
t('onboardingBrand.questions.selectionLimit', {
|
|
limit: selectableQuestionLimitText.value,
|
|
}),
|
|
)
|
|
return
|
|
}
|
|
|
|
row.selected = true
|
|
}
|
|
|
|
async function submitBrand(): Promise<void> {
|
|
const name = brandForm.name.trim()
|
|
const website = brandForm.website.trim()
|
|
const description = brandForm.description.trim()
|
|
|
|
if (!name) {
|
|
message.warning(t('onboardingBrand.messages.nameRequired'))
|
|
return
|
|
}
|
|
if (!description) {
|
|
message.warning(t('onboardingBrand.messages.descriptionRequired'))
|
|
return
|
|
}
|
|
if (brandLimitReached.value) {
|
|
message.warning(
|
|
t('brands.messages.brandLimitReached', {
|
|
limit: brandLibrarySummary.value?.max_brands ?? 0,
|
|
}),
|
|
)
|
|
return
|
|
}
|
|
|
|
try {
|
|
const brand = await createBrandMutation.mutateAsync({
|
|
name,
|
|
website: website || null,
|
|
description,
|
|
})
|
|
createdBrand.value = brand
|
|
brandModalOpen.value = false
|
|
message.success(t('brands.messages.createBrand'))
|
|
await queryClient.invalidateQueries({ queryKey: ['brands'] })
|
|
await companyStore.refreshBrands()
|
|
companyStore.setCurrentBrand(brand.id)
|
|
|
|
beginAiTask()
|
|
try {
|
|
const result = await questionGenerationMutation.mutateAsync({
|
|
brandId: brand.id,
|
|
seedTopic: buildSeedTopic(brand),
|
|
})
|
|
const rows = toCandidateRows(result.candidates)
|
|
applyDefaultSelection(rows)
|
|
candidateRows.value = rows
|
|
questionNotice.value = rows.length ? '' : t('onboardingBrand.questions.empty')
|
|
if (result.ai_points_charged) {
|
|
message.success(
|
|
t('onboardingBrand.messages.aiCharged', {
|
|
count: result.ai_points_charged,
|
|
}),
|
|
)
|
|
}
|
|
} catch (error) {
|
|
candidateRows.value = []
|
|
questionNotice.value = formatError(error)
|
|
} finally {
|
|
finishAiTask()
|
|
questionModalOpen.value = true
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof ApiClientError && error.message === 'brand_limit_reached') {
|
|
message.warning(
|
|
t('brands.messages.brandLimitReached', {
|
|
limit: brandLibrarySummary.value?.max_brands ?? 0,
|
|
}),
|
|
)
|
|
return
|
|
}
|
|
message.error(formatError(error))
|
|
}
|
|
}
|
|
|
|
async function saveSelectedQuestions(): Promise<void> {
|
|
const brand = createdBrand.value
|
|
if (!brand) {
|
|
return
|
|
}
|
|
if (!selectedCandidates.value.length) {
|
|
message.warning(t('onboardingBrand.messages.questionRequired'))
|
|
return
|
|
}
|
|
|
|
try {
|
|
const result = await materializeQuestionsMutation.mutateAsync({
|
|
brandId: brand.id,
|
|
questions: selectedCandidates.value.map((item) => item.text.trim()).filter(Boolean),
|
|
})
|
|
if (result.created_questions <= 0) {
|
|
const skippedAllDuplicates =
|
|
result.skipped_questions.length > 0 &&
|
|
result.skipped_questions.every((item) => isDuplicateQuestionSkipReason(item.reason))
|
|
message.warning(
|
|
skippedAllDuplicates
|
|
? t('brands.questions.errors.duplicateExisting')
|
|
: t('brands.questions.errors.noValidQuestions'),
|
|
)
|
|
return
|
|
}
|
|
message.success(
|
|
t('brands.questions.messages.partialSaved', {
|
|
created: result.created_questions,
|
|
skipped: result.skipped_questions.length,
|
|
}),
|
|
)
|
|
questionModalOpen.value = false
|
|
await enterWorkspace(brand)
|
|
} catch (error) {
|
|
const materializeMessage = getQuestionMaterializeErrorMessage(error, {
|
|
duplicate: t('brands.questions.errors.duplicateExisting'),
|
|
noValid: t('brands.questions.errors.noValidQuestions'),
|
|
})
|
|
if (materializeMessage) {
|
|
message.warning(materializeMessage)
|
|
return
|
|
}
|
|
message.error(formatError(error))
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="brand-onboarding">
|
|
<div class="glow-orb glow-orb--1"></div>
|
|
<div class="glow-orb glow-orb--2"></div>
|
|
|
|
<main class="onboarding-panel">
|
|
<section class="onboarding-panel__intro">
|
|
<div class="brand-mark">
|
|
<ShopOutlined />
|
|
</div>
|
|
<h1>{{ t('onboardingBrand.title') }}</h1>
|
|
<p class="intro-copy">{{ t('onboardingBrand.subtitle') }}</p>
|
|
</section>
|
|
|
|
<section class="brand-entry">
|
|
<div v-if="companyStore.loading && !companyStore.initialized" class="brand-entry__loading">
|
|
<a-skeleton active :paragraph="{ rows: 4 }" />
|
|
</div>
|
|
|
|
<template v-else>
|
|
<div v-if="hasExistingBrands" class="brand-entry__grid">
|
|
<button
|
|
v-for="brand in companyStore.brands"
|
|
:key="brand.id"
|
|
type="button"
|
|
class="brand-choice"
|
|
@click="enterExistingBrand(brand)"
|
|
>
|
|
<span class="brand-choice__icon"><TagsOutlined /></span>
|
|
<strong>{{ brand.name }}</strong>
|
|
<span>{{ brand.website || t('onboardingBrand.noWebsite') }}</span>
|
|
<ArrowRightOutlined class="brand-choice__arrow" />
|
|
</button>
|
|
|
|
<button type="button" class="brand-choice brand-choice--create" @click="openBrandModal">
|
|
<span class="brand-choice__icon"><PlusOutlined /></span>
|
|
<strong>{{ t('onboardingBrand.createBrand') }}</strong>
|
|
<span>{{ t('onboardingBrand.createHint') }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div v-else class="empty-brand-state">
|
|
<button type="button" class="create-brand-card" @click="openBrandModal">
|
|
<span class="create-brand-card__icon"><PlusOutlined /></span>
|
|
<strong>{{ t('onboardingBrand.createBrand') }}</strong>
|
|
<span>{{ t('onboardingBrand.emptyDescription') }}</span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</section>
|
|
</main>
|
|
|
|
<a-modal
|
|
v-model:open="brandModalOpen"
|
|
:title="t('onboardingBrand.form.title')"
|
|
centered
|
|
:footer="null"
|
|
:width="560"
|
|
:mask-closable="false"
|
|
>
|
|
<a-form layout="vertical" class="brand-create-form" @submit.prevent="submitBrand">
|
|
<a-form-item :label="t('onboardingBrand.form.name')" required>
|
|
<a-input
|
|
v-model:value="brandForm.name"
|
|
size="large"
|
|
:placeholder="t('onboardingBrand.form.namePlaceholder')"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item :label="t('onboardingBrand.form.website')">
|
|
<a-input
|
|
v-model:value="brandForm.website"
|
|
size="large"
|
|
:placeholder="t('onboardingBrand.form.websitePlaceholder')"
|
|
>
|
|
<template #prefix><GlobalOutlined /></template>
|
|
</a-input>
|
|
</a-form-item>
|
|
<a-form-item :label="t('onboardingBrand.form.description')" required>
|
|
<a-textarea
|
|
v-model:value="brandForm.description"
|
|
:rows="4"
|
|
:placeholder="t('onboardingBrand.form.descriptionPlaceholder')"
|
|
/>
|
|
</a-form-item>
|
|
<div class="modal-actions">
|
|
<a-button size="large" @click="brandModalOpen = false">
|
|
{{ t('common.cancel') }}
|
|
</a-button>
|
|
<a-button
|
|
size="large"
|
|
type="primary"
|
|
html-type="submit"
|
|
:loading="createBrandMutation.isPending.value"
|
|
>
|
|
{{ t('common.next') }}
|
|
<template #icon><ArrowRightOutlined /></template>
|
|
</a-button>
|
|
</div>
|
|
</a-form>
|
|
</a-modal>
|
|
|
|
<a-modal
|
|
v-model:open="questionModalOpen"
|
|
centered
|
|
:footer="null"
|
|
:width="760"
|
|
:closable="false"
|
|
:mask-closable="false"
|
|
>
|
|
<section class="question-picker">
|
|
<div class="question-picker__header">
|
|
<div>
|
|
<p class="eyebrow">{{ createdBrand?.name }}</p>
|
|
<h2>{{ t('onboardingBrand.questions.title') }}</h2>
|
|
<p>
|
|
{{
|
|
t('onboardingBrand.questions.subtitle', {
|
|
limit: selectableQuestionLimitText,
|
|
})
|
|
}}
|
|
</p>
|
|
</div>
|
|
<div class="question-picker__count">
|
|
<strong>{{ selectedCandidates.length }}</strong>
|
|
<span>/ {{ selectableQuestionLimitText }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<a-alert
|
|
v-if="questionNotice"
|
|
class="question-picker__notice"
|
|
type="warning"
|
|
show-icon
|
|
:message="questionNotice"
|
|
/>
|
|
|
|
<div v-if="candidateRows.length" class="candidate-grid">
|
|
<button
|
|
v-for="row in candidateRows"
|
|
:key="row.id"
|
|
type="button"
|
|
class="candidate-chip"
|
|
:class="{ 'candidate-chip--selected': row.selected }"
|
|
@click="toggleCandidate(row)"
|
|
>
|
|
<CheckCircleOutlined v-if="row.selected" />
|
|
<PlusOutlined v-else />
|
|
<span>{{ row.text }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<a-empty v-else :description="t('onboardingBrand.questions.empty')" />
|
|
|
|
<footer class="question-picker__footer">
|
|
<a-button size="large" @click="enterWorkspace()">
|
|
{{ t('onboardingBrand.questions.skip') }}
|
|
</a-button>
|
|
<a-button
|
|
size="large"
|
|
type="primary"
|
|
:disabled="!selectedCandidates.length"
|
|
:loading="materializeQuestionsMutation.isPending.value"
|
|
@click="saveSelectedQuestions"
|
|
>
|
|
{{ t('onboardingBrand.questions.confirm') }}
|
|
</a-button>
|
|
</footer>
|
|
</section>
|
|
</a-modal>
|
|
|
|
<AiWaitingModal
|
|
:open="aiWaitingOpen"
|
|
:title="t('onboardingBrand.ai.title')"
|
|
:progress="aiProgress"
|
|
:stage="aiStages[aiStageIndex]"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.brand-onboarding {
|
|
position: relative;
|
|
display: flex;
|
|
flex: 1;
|
|
width: 100%;
|
|
min-height: 100vh;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
padding: 40px 24px;
|
|
color: #0f172a;
|
|
background-color: #f8fafc;
|
|
background-image:
|
|
radial-gradient(circle at 10% 20%, rgba(22, 119, 255, 0.05) 0%, transparent 40%),
|
|
radial-gradient(circle at 90% 80%, rgba(99, 102, 241, 0.06) 0%, transparent 50%),
|
|
radial-gradient(circle at 50% 0%, rgba(6, 182, 212, 0.04) 0%, transparent 35%),
|
|
radial-gradient(rgba(148, 163, 184, 0.1) 1px, transparent 1px);
|
|
background-size:
|
|
100% 100%,
|
|
100% 100%,
|
|
100% 100%,
|
|
24px 24px;
|
|
}
|
|
|
|
.glow-orb {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
filter: blur(120px);
|
|
opacity: 0.55;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
animation: float-slow 15s ease-in-out infinite alternate;
|
|
}
|
|
|
|
.glow-orb--1 {
|
|
top: -10%;
|
|
left: 15%;
|
|
width: 500px;
|
|
height: 500px;
|
|
background: radial-gradient(circle, rgba(22, 119, 255, 0.15) 0%, rgba(99, 102, 241, 0.05) 100%);
|
|
}
|
|
|
|
.glow-orb--2 {
|
|
bottom: -10%;
|
|
right: 10%;
|
|
width: 600px;
|
|
height: 600px;
|
|
background: radial-gradient(circle, rgba(6, 182, 212, 0.12) 0%, rgba(22, 119, 255, 0.04) 100%);
|
|
animation-delay: -5s;
|
|
}
|
|
|
|
@keyframes float-slow {
|
|
0% {
|
|
transform: translate(0, 0) scale(1);
|
|
}
|
|
100% {
|
|
transform: translate(30px, -30px) scale(1.05);
|
|
}
|
|
}
|
|
|
|
.onboarding-panel {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: min(520px, 100%);
|
|
padding: 48px;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
backdrop-filter: blur(24px);
|
|
-webkit-backdrop-filter: blur(24px);
|
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
|
border-radius: 24px;
|
|
box-shadow:
|
|
0 1px 2px rgba(0, 0, 0, 0.02),
|
|
0 4px 12px rgba(0, 0, 0, 0.02),
|
|
0 16px 36px rgba(22, 119, 255, 0.04),
|
|
0 32px 80px rgba(15, 23, 42, 0.05);
|
|
}
|
|
|
|
.onboarding-panel__intro {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
margin-bottom: 36px;
|
|
}
|
|
|
|
.brand-mark {
|
|
display: flex;
|
|
width: 52px;
|
|
height: 52px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: linear-gradient(135deg, rgba(22, 119, 255, 0.08) 0%, rgba(99, 102, 241, 0.08) 100%);
|
|
border: 1px solid rgba(22, 119, 255, 0.15);
|
|
border-radius: 14px;
|
|
color: #1677ff;
|
|
font-size: 22px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.04);
|
|
}
|
|
|
|
.eyebrow {
|
|
display: inline-block;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: #1677ff;
|
|
margin: 0 0 10px;
|
|
background: rgba(22, 119, 255, 0.06);
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(22, 119, 255, 0.1);
|
|
}
|
|
|
|
.onboarding-panel h1 {
|
|
margin: 0;
|
|
color: #0f172a;
|
|
font-size: 26px;
|
|
font-weight: 700;
|
|
line-height: 1.25;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.intro-copy {
|
|
margin: 12px 0 0;
|
|
color: #475569;
|
|
font-size: 15px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.brand-entry {
|
|
display: flex;
|
|
align-items: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.brand-entry__loading,
|
|
.brand-entry__grid,
|
|
.empty-brand-state {
|
|
width: 100%;
|
|
}
|
|
|
|
.brand-entry__grid {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.brand-choice {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr auto;
|
|
grid-template-rows: auto auto;
|
|
align-items: center;
|
|
column-gap: 16px;
|
|
width: 100%;
|
|
padding: 16px 20px;
|
|
background: rgba(255, 255, 255, 0.6);
|
|
border: 1px solid rgba(226, 232, 240, 0.8);
|
|
border-radius: 16px;
|
|
cursor: pointer;
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
text-align: left;
|
|
}
|
|
|
|
.brand-choice:hover {
|
|
background: #ffffff;
|
|
border-color: rgba(22, 119, 255, 0.4);
|
|
box-shadow:
|
|
0 10px 25px -5px rgba(22, 119, 255, 0.08),
|
|
0 4px 12px -5px rgba(22, 119, 255, 0.05);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.brand-choice__icon {
|
|
grid-column: 1;
|
|
grid-row: 1 / span 2;
|
|
display: flex;
|
|
width: 44px;
|
|
height: 44px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #1677ff;
|
|
font-size: 18px;
|
|
background: rgba(22, 119, 255, 0.06);
|
|
border: 1px solid rgba(22, 119, 255, 0.1);
|
|
border-radius: 12px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.brand-choice:hover .brand-choice__icon {
|
|
background: rgba(22, 119, 255, 0.15);
|
|
border-color: rgba(22, 119, 255, 0.25);
|
|
color: #1677ff;
|
|
}
|
|
|
|
.brand-choice strong {
|
|
grid-column: 2;
|
|
grid-row: 1;
|
|
margin: 0;
|
|
color: #0f172a;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.brand-choice span:not(.brand-choice__icon) {
|
|
grid-column: 2;
|
|
grid-row: 2;
|
|
color: #64748b;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.brand-choice__arrow {
|
|
grid-column: 3;
|
|
grid-row: 1 / span 2;
|
|
color: #94a3b8;
|
|
font-size: 14px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.brand-choice:hover .brand-choice__arrow {
|
|
color: #1677ff;
|
|
transform: translateX(4px);
|
|
}
|
|
|
|
.brand-choice--create {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
border: 1px dashed rgba(22, 119, 255, 0.3);
|
|
background: rgba(22, 119, 255, 0.02);
|
|
min-height: auto;
|
|
padding: 16px;
|
|
border-radius: 16px;
|
|
cursor: pointer;
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.brand-choice--create:hover {
|
|
background: rgba(22, 119, 255, 0.06);
|
|
border-color: #1677ff;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 25px -5px rgba(22, 119, 255, 0.05);
|
|
}
|
|
|
|
.brand-choice--create .brand-choice__icon {
|
|
width: auto;
|
|
height: auto;
|
|
background: transparent;
|
|
color: #1677ff;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.brand-choice--create strong {
|
|
margin: 0;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: #1677ff;
|
|
}
|
|
|
|
.brand-choice--create span:not(.brand-choice__icon) {
|
|
display: none;
|
|
}
|
|
|
|
.empty-brand-state {
|
|
display: flex;
|
|
justify-content: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.create-brand-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
width: 100%;
|
|
padding: 40px 24px;
|
|
background: rgba(255, 255, 255, 0.4);
|
|
border: 1px dashed rgba(22, 119, 255, 0.25);
|
|
border-radius: 20px;
|
|
cursor: pointer;
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.create-brand-card:hover {
|
|
background: rgba(255, 255, 255, 0.9);
|
|
border-color: #1677ff;
|
|
box-shadow:
|
|
0 12px 30px -5px rgba(22, 119, 255, 0.08),
|
|
0 4px 12px -5px rgba(22, 119, 255, 0.04);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.create-brand-card__icon {
|
|
font-size: 24px;
|
|
color: #1677ff;
|
|
background: rgba(22, 119, 255, 0.06);
|
|
border: 1px solid rgba(22, 119, 255, 0.1);
|
|
width: 56px;
|
|
height: 56px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 16px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.create-brand-card:hover .create-brand-card__icon {
|
|
background: rgba(22, 119, 255, 0.15);
|
|
border-color: rgba(22, 119, 255, 0.25);
|
|
color: #1677ff;
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.create-brand-card strong {
|
|
color: #0f172a;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.create-brand-card span:not(.create-brand-card__icon) {
|
|
color: #64748b;
|
|
font-size: 13px;
|
|
text-align: center;
|
|
max-width: 280px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.brand-create-form {
|
|
padding-top: 12px;
|
|
}
|
|
|
|
.brand-create-form :deep(.ant-form-item-label > label) {
|
|
font-weight: 600;
|
|
color: #334155;
|
|
}
|
|
|
|
.brand-create-form :deep(.ant-input),
|
|
.brand-create-form :deep(.ant-input-affix-wrapper) {
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.brand-create-form :deep(.ant-input-affix-wrapper) {
|
|
padding: 8px 12px;
|
|
}
|
|
|
|
.modal-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 12px;
|
|
margin-top: 28px;
|
|
}
|
|
|
|
.modal-actions :deep(.ant-btn) {
|
|
border-radius: 10px;
|
|
height: 44px;
|
|
padding: 0 24px;
|
|
font-weight: 600;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.modal-actions :deep(.ant-btn-primary) {
|
|
background: #1677ff;
|
|
border-color: #1677ff;
|
|
box-shadow: 0 4px 12px rgba(22, 119, 255, 0.15);
|
|
}
|
|
|
|
.modal-actions :deep(.ant-btn-primary:hover) {
|
|
background: #4096ff;
|
|
border-color: #4096ff;
|
|
box-shadow: 0 6px 16px rgba(22, 119, 255, 0.25);
|
|
}
|
|
|
|
.question-picker {
|
|
padding: 12px 8px;
|
|
}
|
|
|
|
.question-picker__header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 20px;
|
|
border-bottom: 1px solid rgba(226, 232, 240, 0.6);
|
|
padding-bottom: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.question-picker__header h2 {
|
|
margin: 0;
|
|
color: #0f172a;
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.question-picker__header p:not(.eyebrow) {
|
|
margin: 6px 0 0;
|
|
color: #64748b;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.question-picker__header .eyebrow {
|
|
display: inline-block;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: #64748b;
|
|
margin-bottom: 6px;
|
|
background: #f1f5f9;
|
|
padding: 2px 8px;
|
|
border-radius: 6px;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.question-picker__count {
|
|
min-width: auto;
|
|
padding: 6px 14px;
|
|
color: #1677ff;
|
|
text-align: center;
|
|
background: rgba(22, 119, 255, 0.08);
|
|
border: 1px solid rgba(22, 119, 255, 0.15);
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 4px;
|
|
}
|
|
|
|
.question-picker__count strong {
|
|
font-size: 20px;
|
|
line-height: 1;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.question-picker__count span {
|
|
color: rgba(22, 119, 255, 0.6);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.question-picker__notice {
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.candidate-grid {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
max-height: min(46vh, 420px);
|
|
gap: 10px;
|
|
margin-top: 16px;
|
|
overflow-y: auto;
|
|
padding: 4px;
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
.candidate-chip {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 16px;
|
|
color: #334155;
|
|
text-align: left;
|
|
background: rgba(241, 245, 249, 0.8);
|
|
border: 1px solid rgba(226, 232, 240, 0.8);
|
|
border-radius: 20px;
|
|
cursor: pointer;
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
min-height: 38px;
|
|
}
|
|
|
|
.candidate-chip:hover {
|
|
background: #e2e8f0;
|
|
border-color: #cbd5e1;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.candidate-chip svg {
|
|
flex: 0 0 auto;
|
|
font-size: 13px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.candidate-chip span {
|
|
min-width: 0;
|
|
overflow-wrap: anywhere;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.candidate-chip--selected {
|
|
color: #1677ff;
|
|
background: rgba(22, 119, 255, 0.08);
|
|
border-color: rgba(22, 119, 255, 0.3);
|
|
}
|
|
|
|
.candidate-chip--selected:hover {
|
|
background: rgba(22, 119, 255, 0.12);
|
|
border-color: rgba(22, 119, 255, 0.4);
|
|
}
|
|
|
|
.candidate-chip--selected svg {
|
|
color: #1677ff;
|
|
}
|
|
|
|
.question-picker__footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 12px;
|
|
margin-top: 24px;
|
|
border-top: 1px solid rgba(226, 232, 240, 0.6);
|
|
padding-top: 20px;
|
|
}
|
|
|
|
.question-picker__footer :deep(.ant-btn) {
|
|
border-radius: 10px;
|
|
height: 40px;
|
|
padding: 0 20px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.onboarding-panel {
|
|
padding: 32px 24px;
|
|
}
|
|
}
|
|
</style>
|