feat(questions): make brand questions the primary monitoring & template axis
- add /questions/combination-fill endpoint with AI-driven, IP-region-aware matrix fill - extract ip2region resolver from ops/app into shared/ipregion for cross-service use - thread question_id filter through dashboard composite, citation summary, and collect-now - switch template wizard from keyword inputs to brand-question selection (primary + supplemental) - pass brand_question and supplemental_questions through assist/title/outline prompts - add AiWaitingModal + 45s client timeout and request_timeout error mapping for long AI flows Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
import type {
|
||||
ArticleDetail,
|
||||
JsonValue,
|
||||
Question,
|
||||
TemplateAnalyzeResult,
|
||||
TemplateAssistCompetitor,
|
||||
TemplateOutlineNode,
|
||||
@@ -20,6 +21,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import AiWaitingModal from '@/components/AiWaitingModal.vue'
|
||||
import KnowledgeGroupSelect from '@/components/KnowledgeGroupSelect.vue'
|
||||
import { articlesApi, brandsApi, normalizeInputParams, templatesApi } from '@/lib/api'
|
||||
import { getTemplateMeta } from '@/lib/display'
|
||||
@@ -170,7 +172,8 @@ const brandName = ref('')
|
||||
const officialWebsite = ref('')
|
||||
const brandSummary = ref('')
|
||||
|
||||
const keywordDrafts = ref<string[]>([])
|
||||
const primaryQuestionId = ref<number | null>(null)
|
||||
const supplementalQuestionIds = ref<number[]>([])
|
||||
const competitorDrafts = ref<DraftCompetitor[]>([])
|
||||
|
||||
const assistTitles = ref<string[]>([])
|
||||
@@ -218,10 +221,10 @@ const draftArticleQuery = useQuery({
|
||||
queryFn: () => articlesApi.detail(draftArticleId.value as number),
|
||||
})
|
||||
|
||||
const keywordsQuery = useQuery({
|
||||
queryKey: computed(() => ['brands', selectedBrandId.value, 'keywords']),
|
||||
const questionsQuery = useQuery({
|
||||
queryKey: computed(() => ['brands', selectedBrandId.value, 'questions', 'wizard']),
|
||||
enabled: computed(() => enabled.value && Boolean(selectedBrandId.value)),
|
||||
queryFn: () => brandsApi.listKeywords(selectedBrandId.value as number),
|
||||
queryFn: () => brandsApi.listQuestions(selectedBrandId.value as number),
|
||||
})
|
||||
|
||||
const competitorsQuery = useQuery({
|
||||
@@ -284,18 +287,41 @@ const selectedBrand = computed(
|
||||
() => brandsQuery.data.value?.find((brand) => brand.id === selectedBrandId.value) ?? null,
|
||||
)
|
||||
|
||||
const keywordOptions = computed(() =>
|
||||
(keywordsQuery.data.value ?? []).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
function findQuestionById(id: number | null): Question | null {
|
||||
if (!id) {
|
||||
return null
|
||||
}
|
||||
return questionsQuery.data.value?.find((item) => item.id === id) ?? null
|
||||
}
|
||||
|
||||
const questionOptions = computed(() =>
|
||||
(questionsQuery.data.value ?? []).map((item) => ({
|
||||
label: item.question_text,
|
||||
value: item.id,
|
||||
})),
|
||||
)
|
||||
const supplementalQuestionOptions = computed(() =>
|
||||
questionOptions.value.filter((item) => item.value !== primaryQuestionId.value),
|
||||
)
|
||||
const primaryQuestion = computed(() => findQuestionById(primaryQuestionId.value))
|
||||
const supplementalQuestions = computed(() =>
|
||||
supplementalQuestionIds.value
|
||||
.map((id) => findQuestionById(id))
|
||||
.filter((item): item is Question => Boolean(item)),
|
||||
)
|
||||
const primaryQuestionText = computed(() => primaryQuestion.value?.question_text.trim() || '')
|
||||
const supplementalQuestionTexts = computed(() =>
|
||||
supplementalQuestions.value.map((item) => item.question_text.trim()).filter(Boolean),
|
||||
)
|
||||
const selectedQuestionTexts = computed(() =>
|
||||
dedupeStrings([primaryQuestionText.value, ...supplementalQuestionTexts.value]),
|
||||
)
|
||||
|
||||
const normalizedBrandName = computed(
|
||||
() => brandName.value.trim() || selectedBrand.value?.name || '',
|
||||
)
|
||||
const finalTitle = computed(() => customTitle.value.trim() || selectedTitle.value.trim())
|
||||
const primaryKeyword = computed(() => keywordDrafts.value[0]?.trim() || '')
|
||||
const primaryKeyword = computed(() => primaryQuestionText.value)
|
||||
const showCompetitorsCard = computed(
|
||||
() =>
|
||||
!isResearchReportTemplate.value &&
|
||||
@@ -333,7 +359,8 @@ const templateRenderContext = computed(() =>
|
||||
brandName: normalizedBrandName.value,
|
||||
officialWebsite: officialWebsite.value,
|
||||
brandSummary: brandSummary.value,
|
||||
primaryKeyword: primaryKeyword.value,
|
||||
primaryQuestion: primaryQuestionText.value,
|
||||
supplementalQuestions: supplementalQuestionTexts.value,
|
||||
inputParams: buildStructureInputParams(),
|
||||
competitorCount: buildCompetitorPayload().length,
|
||||
}),
|
||||
@@ -389,16 +416,16 @@ const generateStepCopy = computed<TemplateStepConfig>(
|
||||
() => wizardConfig.value?.steps?.generate ?? {},
|
||||
)
|
||||
const brandCardCopy = computed<TemplateCardCopy>(
|
||||
() => wizardConfig.value?.basic?.cards?.brand ?? {},
|
||||
() => sanitizeLegacyKeywordCopy(wizardConfig.value?.basic?.cards?.brand ?? {}),
|
||||
)
|
||||
const keywordCardCopy = computed<TemplateCardCopy>(
|
||||
() => wizardConfig.value?.basic?.cards?.keywords ?? {},
|
||||
const questionCardCopy = computed<TemplateCardCopy>(
|
||||
() => sanitizeQuestionCardCopy(wizardConfig.value?.basic?.cards?.keywords ?? {}),
|
||||
)
|
||||
const competitorsCardCopy = computed<TemplateCardCopy>(
|
||||
() => wizardConfig.value?.basic?.cards?.competitors ?? {},
|
||||
)
|
||||
const titleCardCopy = computed<TemplateCardCopy>(
|
||||
() => wizardConfig.value?.structure?.cards?.choose_title ?? {},
|
||||
() => sanitizeLegacyKeywordCopy(wizardConfig.value?.structure?.cards?.choose_title ?? {}),
|
||||
)
|
||||
const outlineCardCopy = computed<TemplateCardCopy>(
|
||||
() => wizardConfig.value?.structure?.cards?.outline ?? {},
|
||||
@@ -442,7 +469,8 @@ const hasWizardDraftContent = computed(
|
||||
Boolean(brandName.value.trim()) ||
|
||||
Boolean(officialWebsite.value.trim()) ||
|
||||
Boolean(brandSummary.value.trim()) ||
|
||||
keywordDrafts.value.some((item) => item.trim()) ||
|
||||
Boolean(primaryQuestionId.value) ||
|
||||
supplementalQuestionIds.value.length > 0 ||
|
||||
competitorDrafts.value.some(hasCompetitorDraftContent) ||
|
||||
assistTitles.value.length > 0 ||
|
||||
Boolean(customTitle.value.trim()) ||
|
||||
@@ -456,7 +484,7 @@ const assistStages = computed(() =>
|
||||
assistTaskKind.value === 'analyze'
|
||||
? [
|
||||
t('templates.wizard.assist.analyzeStages.brand'),
|
||||
t('templates.wizard.assist.analyzeStages.keywords'),
|
||||
t('templates.wizard.assist.analyzeStages.questions'),
|
||||
showCompetitorsCard.value
|
||||
? t('templates.wizard.assist.analyzeStages.competitors')
|
||||
: t('templates.wizard.assist.analyzeStages.context'),
|
||||
@@ -495,7 +523,8 @@ function resetWizardState(detail: NonNullable<typeof templateDetail.value>): voi
|
||||
brandName.value = ''
|
||||
officialWebsite.value = ''
|
||||
brandSummary.value = ''
|
||||
keywordDrafts.value = []
|
||||
primaryQuestionId.value = null
|
||||
supplementalQuestionIds.value = []
|
||||
competitorDrafts.value = []
|
||||
assistTitles.value = []
|
||||
selectedTitle.value = ''
|
||||
@@ -515,7 +544,8 @@ function resetWizardState(detail: NonNullable<typeof templateDetail.value>): voi
|
||||
brandName: '',
|
||||
officialWebsite: '',
|
||||
brandSummary: '',
|
||||
primaryKeyword: '',
|
||||
primaryQuestion: '',
|
||||
supplementalQuestions: [],
|
||||
inputParams: {},
|
||||
competitorCount: 0,
|
||||
}),
|
||||
@@ -530,7 +560,8 @@ function resetWizardState(detail: NonNullable<typeof templateDetail.value>): voi
|
||||
brandName: '',
|
||||
officialWebsite: '',
|
||||
brandSummary: '',
|
||||
primaryKeyword: '',
|
||||
primaryQuestion: '',
|
||||
supplementalQuestions: [],
|
||||
inputParams: {},
|
||||
competitorCount: 0,
|
||||
}),
|
||||
@@ -553,7 +584,9 @@ function applyDraftArticleState(detail: ArticleDetail): void {
|
||||
brandName.value = stringFromUnknown(draftState.brand_name) || ''
|
||||
officialWebsite.value = stringFromUnknown(draftState.official_website) || ''
|
||||
brandSummary.value = stringFromUnknown(draftState.brand_summary) || ''
|
||||
keywordDrafts.value = stringListFromUnknown(draftState.keyword_drafts) ?? []
|
||||
primaryQuestionId.value = numberFromUnknown(draftState.primary_question_id) ?? null
|
||||
supplementalQuestionIds.value =
|
||||
numberListFromUnknown(draftState.supplemental_question_ids)?.slice(0, 3) ?? []
|
||||
competitorDrafts.value = parseDraftCompetitors(draftState.competitor_drafts)
|
||||
assistTitles.value = stringListFromUnknown(draftState.assist_titles) ?? []
|
||||
selectedTitle.value = stringFromUnknown(draftState.selected_title) || ''
|
||||
@@ -619,6 +652,29 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(primaryQuestionId, (id) => {
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
supplementalQuestionIds.value = supplementalQuestionIds.value.filter((item) => item !== id)
|
||||
})
|
||||
|
||||
watch(supplementalQuestionIds, (ids) => {
|
||||
const optionValues = supplementalQuestionOptions.value.map((item) => item.value)
|
||||
const allowedIds = new Set(optionValues)
|
||||
const shouldCheckOptions = optionValues.length > 0
|
||||
const normalized = ids.filter(
|
||||
(id, index) =>
|
||||
id !== primaryQuestionId.value &&
|
||||
ids.indexOf(id) === index &&
|
||||
(!shouldCheckOptions || allowedIds.has(id)),
|
||||
)
|
||||
const limited = normalized.slice(0, 3)
|
||||
if (limited.length !== ids.length || limited.some((id, index) => id !== ids[index])) {
|
||||
supplementalQuestionIds.value = limited
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedBrandId, async (brandId) => {
|
||||
if (restoringDraft.value) {
|
||||
return
|
||||
@@ -643,9 +699,9 @@ watch(selectedBrandId, async (brandId) => {
|
||||
brandSummary.value = brand.description
|
||||
}
|
||||
|
||||
const keywordsResult = await keywordsQuery.refetch()
|
||||
|
||||
keywordDrafts.value = dedupeStrings((keywordsResult.data ?? []).map((item) => item.name))
|
||||
primaryQuestionId.value = null
|
||||
supplementalQuestionIds.value = []
|
||||
await questionsQuery.refetch()
|
||||
|
||||
if (!showCompetitorsCard.value) {
|
||||
competitorDrafts.value = []
|
||||
@@ -759,6 +815,14 @@ function validateBasicInfo(): boolean {
|
||||
message.warning(t('templates.wizard.messages.missingBrand'))
|
||||
return false
|
||||
}
|
||||
if (!selectedBrandId.value) {
|
||||
message.warning(t('templates.wizard.messages.selectBrandBeforeQuestion'))
|
||||
return false
|
||||
}
|
||||
if (!primaryQuestionId.value || !primaryQuestionText.value) {
|
||||
message.warning(t('templates.wizard.messages.missingPrimaryQuestion'))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -859,8 +923,8 @@ async function runAnalyzeTask(notifySuccess: boolean): Promise<void> {
|
||||
brand_id: selectedBrandId.value,
|
||||
brand_name: normalizedBrandName.value,
|
||||
website: officialWebsite.value.trim() || null,
|
||||
brand_question: primaryQuestionText.value,
|
||||
input_params: buildAssistInputParams(),
|
||||
existing_keywords: keywordDrafts.value,
|
||||
existing_competitors: competitorPayload.length > 0 ? competitorPayload : undefined,
|
||||
})
|
||||
|
||||
@@ -893,8 +957,9 @@ async function runTitleTask(): Promise<void> {
|
||||
brand_name: normalizedBrandName.value,
|
||||
website: officialWebsite.value.trim() || null,
|
||||
brand_summary: brandSummary.value.trim() || null,
|
||||
input_params: buildAssistInputParams(),
|
||||
keywords: keywordDrafts.value,
|
||||
brand_question: primaryQuestionText.value,
|
||||
input_params: buildTitleInputParams(),
|
||||
keywords: primaryQuestionText.value ? [primaryQuestionText.value] : [],
|
||||
competitors: competitorPayload.length > 0 ? competitorPayload : undefined,
|
||||
})
|
||||
|
||||
@@ -928,8 +993,10 @@ async function runOutlineTask(): Promise<void> {
|
||||
website: officialWebsite.value.trim() || null,
|
||||
brand_summary: brandSummary.value.trim() || null,
|
||||
title: finalTitle.value,
|
||||
brand_question: primaryQuestionText.value,
|
||||
supplemental_questions: supplementalQuestionTexts.value,
|
||||
input_params: buildStructureInputParams(),
|
||||
keywords: keywordDrafts.value,
|
||||
keywords: selectedQuestionTexts.value,
|
||||
competitors: competitorPayload.length > 0 ? competitorPayload : undefined,
|
||||
outline_sections: selectedOutlineLabels.value,
|
||||
})
|
||||
@@ -1019,7 +1086,6 @@ function applyAnalyzeResult(result: TemplateAnalyzeResult): void {
|
||||
brandSummary.value = result.brand_summary.trim()
|
||||
}
|
||||
|
||||
keywordDrafts.value = dedupeStrings([...keywordDrafts.value, ...(result.keywords ?? [])])
|
||||
if (showCompetitorsCard.value) {
|
||||
competitorDrafts.value = mergeCompetitors(competitorDrafts.value, result.competitors ?? [])
|
||||
}
|
||||
@@ -1097,8 +1163,42 @@ function buildCompetitorPayload(): TemplateAssistCompetitor[] {
|
||||
}))
|
||||
}
|
||||
|
||||
function sanitizeQuestionCardCopy(copy: TemplateCardCopy): TemplateCardCopy {
|
||||
if (copy.title?.includes('关键词') || copy.hint?.includes('关键词')) {
|
||||
return {
|
||||
...copy,
|
||||
title: undefined,
|
||||
hint: undefined,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...copy,
|
||||
title: sanitizeLegacyKeywordText(copy.title) || undefined,
|
||||
hint: sanitizeLegacyKeywordText(copy.hint) || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeLegacyKeywordCopy(copy: TemplateCardCopy): TemplateCardCopy {
|
||||
return {
|
||||
...copy,
|
||||
title: sanitizeLegacyKeywordText(copy.title) || undefined,
|
||||
hint: sanitizeLegacyKeywordText(copy.hint) || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeLegacyKeywordText(value: string | undefined): string {
|
||||
return (value || '')
|
||||
.replace(/核心关键词/g, '品牌主问题')
|
||||
.replace(/关键词/g, '品牌问题')
|
||||
.replace(/标题生成/g, '后续生成')
|
||||
}
|
||||
|
||||
function buildAssistInputParams(): Record<string, JsonValue> {
|
||||
const payload: Record<string, JsonValue> = {}
|
||||
const payload: Record<string, JsonValue> = {
|
||||
brand_question: primaryQuestionText.value,
|
||||
primary_question: primaryQuestionText.value,
|
||||
primary_keyword: primaryQuestionText.value,
|
||||
}
|
||||
const derivedInputs = wizardConfig.value?.derived_inputs ?? {}
|
||||
for (const [key, config] of Object.entries(derivedInputs)) {
|
||||
const value = resolveDerivedInputValue(key, config)
|
||||
@@ -1110,8 +1210,16 @@ function buildAssistInputParams(): Record<string, JsonValue> {
|
||||
return payload
|
||||
}
|
||||
|
||||
function buildTitleInputParams(): Record<string, JsonValue> {
|
||||
return buildAssistInputParams()
|
||||
}
|
||||
|
||||
function buildStructureInputParams(): Record<string, JsonValue> {
|
||||
const payload = buildAssistInputParams()
|
||||
if (supplementalQuestionTexts.value.length > 0) {
|
||||
payload.supplemental_questions = supplementalQuestionTexts.value
|
||||
payload.keywords = selectedQuestionTexts.value
|
||||
}
|
||||
if (reviewIntroHookPromptValue.value) {
|
||||
payload[reviewIntroHookFieldName.value] = reviewIntroHookPromptValue.value
|
||||
}
|
||||
@@ -1141,8 +1249,12 @@ function buildPayload(): Record<string, JsonValue> {
|
||||
brand_name: normalizedBrandName.value || undefined,
|
||||
brand_summary: brandSummary.value.trim() || undefined,
|
||||
official_website: officialWebsite.value.trim() || undefined,
|
||||
primary_keyword: primaryKeyword.value || undefined,
|
||||
keywords: keywordDrafts.value.length > 0 ? dedupeStrings(keywordDrafts.value) : undefined,
|
||||
brand_question: primaryQuestionText.value || undefined,
|
||||
primary_question: primaryQuestionText.value || undefined,
|
||||
supplemental_questions:
|
||||
supplementalQuestionTexts.value.length > 0 ? supplementalQuestionTexts.value : undefined,
|
||||
primary_keyword: primaryQuestionText.value || undefined,
|
||||
keywords: selectedQuestionTexts.value.length > 0 ? selectedQuestionTexts.value : undefined,
|
||||
[reviewIntroHookFieldName.value]: reviewIntroHookPromptValue.value || undefined,
|
||||
knowledge_group_ids:
|
||||
selectedKnowledgeGroupIds.value.length > 0 ? selectedKnowledgeGroupIds.value : undefined,
|
||||
@@ -1173,7 +1285,10 @@ function buildDraftState(): Record<string, JsonValue> {
|
||||
brand_name: brandName.value.trim(),
|
||||
official_website: officialWebsite.value.trim(),
|
||||
brand_summary: brandSummary.value.trim(),
|
||||
keyword_drafts: dedupeStrings(keywordDrafts.value),
|
||||
primary_question_id: primaryQuestionId.value,
|
||||
primary_question_text: primaryQuestionText.value,
|
||||
supplemental_question_ids: supplementalQuestionIds.value,
|
||||
supplemental_question_texts: supplementalQuestionTexts.value,
|
||||
competitor_drafts: showCompetitorsCard.value
|
||||
? competitorDrafts.value.map((item) => ({
|
||||
library_id: item.libraryId ?? null,
|
||||
@@ -1945,17 +2060,23 @@ function buildTemplateRenderContext(input: {
|
||||
brandName: string
|
||||
officialWebsite: string
|
||||
brandSummary: string
|
||||
primaryKeyword: string
|
||||
primaryQuestion: string
|
||||
supplementalQuestions: string[]
|
||||
inputParams: Record<string, JsonValue>
|
||||
competitorCount: number
|
||||
}): Record<string, string> {
|
||||
const primaryQuestion = input.primaryQuestion.trim()
|
||||
const supplementalQuestions = dedupeStrings(input.supplementalQuestions)
|
||||
const context: Record<string, string> = {
|
||||
locale: input.locale.trim(),
|
||||
current_year: String(new Date().getFullYear()),
|
||||
brand_name: input.brandName.trim(),
|
||||
official_website: input.officialWebsite.trim(),
|
||||
brand_summary: input.brandSummary.trim(),
|
||||
primary_keyword: input.primaryKeyword.trim() || input.brandName.trim(),
|
||||
brand_question: primaryQuestion,
|
||||
primary_question: primaryQuestion,
|
||||
primary_keyword: primaryQuestion || input.brandName.trim(),
|
||||
supplemental_questions: supplementalQuestions.join(', '),
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(input.inputParams)) {
|
||||
@@ -2338,17 +2459,60 @@ function onStructureDragEnd(): void {
|
||||
<div class="wizard-card">
|
||||
<div class="wizard-card__header">
|
||||
<div>
|
||||
<h3>{{ keywordCardCopy.title || t('templates.wizard.sections.keywords') }}</h3>
|
||||
<p>{{ keywordCardCopy.hint || t('templates.wizard.hints.keywords') }}</p>
|
||||
<h3>
|
||||
{{ questionCardCopy.title || t('templates.wizard.sections.brandQuestions') }}
|
||||
</h3>
|
||||
<p>
|
||||
{{ questionCardCopy.hint || t('templates.wizard.hints.brandQuestions') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="brand-question-panel">
|
||||
<div class="field-item">
|
||||
<label class="required-asterisk">
|
||||
{{ t('templates.wizard.sections.primaryQuestion') }}
|
||||
</label>
|
||||
<a-select
|
||||
v-model:value="primaryQuestionId"
|
||||
allow-clear
|
||||
show-search
|
||||
:disabled="!selectedBrandId"
|
||||
:loading="questionsQuery.isPending.value"
|
||||
:not-found-content="
|
||||
selectedBrandId
|
||||
? t('templates.wizard.hints.questionEmpty')
|
||||
: t('templates.wizard.hints.selectBrandFirst')
|
||||
"
|
||||
:options="questionOptions"
|
||||
:placeholder="t('templates.wizard.placeholders.primaryQuestion')"
|
||||
option-filter-prop="label"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field-item">
|
||||
<div class="optional-field-label">
|
||||
<label>{{ t('templates.wizard.sections.supplementalQuestions') }}</label>
|
||||
<span>{{ supplementalQuestionIds.length }}/3</span>
|
||||
</div>
|
||||
<a-select
|
||||
v-model:value="supplementalQuestionIds"
|
||||
mode="multiple"
|
||||
show-search
|
||||
:disabled="!selectedBrandId || !primaryQuestionId"
|
||||
:loading="questionsQuery.isPending.value"
|
||||
:max-tag-count="3"
|
||||
:not-found-content="t('templates.wizard.hints.questionEmpty')"
|
||||
:options="supplementalQuestionOptions"
|
||||
:placeholder="t('templates.wizard.placeholders.supplementalQuestions')"
|
||||
option-filter-prop="label"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<p class="field-helper">
|
||||
{{ t('templates.wizard.hints.supplementalQuestions') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-select
|
||||
v-model:value="keywordDrafts"
|
||||
mode="tags"
|
||||
:options="keywordOptions"
|
||||
:placeholder="t('templates.wizard.placeholders.keywords')"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="showCompetitorsCard" class="wizard-card">
|
||||
@@ -2789,22 +2953,12 @@ function onStructureDragEnd(): void {
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<a-modal
|
||||
<AiWaitingModal
|
||||
:open="assistModalOpen"
|
||||
:footer="null"
|
||||
:closable="false"
|
||||
:mask-closable="false"
|
||||
centered
|
||||
width="640px"
|
||||
wrap-class-name="wizard-assist-modal"
|
||||
>
|
||||
<div class="assist-modal">
|
||||
<div class="assist-orb" />
|
||||
<h3>{{ assistHeading }}</h3>
|
||||
<a-progress :percent="assistProgress" status="active" :show-info="false" />
|
||||
<p>{{ assistStages[assistStageIndex] }}</p>
|
||||
</div>
|
||||
</a-modal>
|
||||
:title="assistHeading"
|
||||
:progress="assistProgress"
|
||||
:stage="assistStages[assistStageIndex]"
|
||||
/>
|
||||
|
||||
<a-modal
|
||||
:open="leaveConfirmOpen"
|
||||
@@ -2997,12 +3151,50 @@ function onStructureDragEnd(): void {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
.optional-field-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.optional-field-label label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #1f2937;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.optional-field-label label::after {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
.optional-field-label span {
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-helper {
|
||||
margin: 0;
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.required-asterisk::before {
|
||||
margin-right: 4px;
|
||||
color: #ff4d4f;
|
||||
content: '*';
|
||||
}
|
||||
|
||||
.brand-question-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.wizard-ai-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3574,51 +3766,6 @@ function onStructureDragEnd(): void {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.assist-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 24px 20px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.assist-orb {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
border-radius: 999px;
|
||||
background:
|
||||
radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.92), transparent 38%),
|
||||
conic-gradient(from 120deg, #4f8fff, #7dd3fc, #e879f9, #4f8fff);
|
||||
box-shadow:
|
||||
0 18px 38px rgba(79, 143, 255, 0.2),
|
||||
inset 0 0 18px rgba(255, 255, 255, 0.4);
|
||||
animation: orb-float 2.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.assist-modal h3 {
|
||||
margin: 0;
|
||||
color: #1b2741;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.assist-modal p {
|
||||
margin: 0;
|
||||
color: #5f6f8a;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
@keyframes orb-float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px) scale(1.04);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.wizard-section--structure {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -3628,6 +3775,10 @@ function onStructureDragEnd(): void {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.brand-question-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.competitor-table__head,
|
||||
.competitor-table__row {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -3678,9 +3829,5 @@ function onStructureDragEnd(): void {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.assist-modal h3 {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user