feat(admin-brand): add question expansion service and related functionality
- Implemented QuestionExpansionService for generating and materializing questions based on combinations and AI distillation. - Added question metadata classification logic to infer intent and layer of questions. - Created API handlers for question expansion operations including combination preview, AI distillation, metadata classification, and materialization. - Introduced database migrations to support new question-related fields and constraints in brand_questions and brand_keywords tables. - Added caching mechanism for AI distillation results to improve performance. - Defined JSON schemas for question distillation responses to ensure data integrity.
This commit is contained in:
@@ -212,6 +212,10 @@ const enUS = {
|
||||
title: 'Brand Library',
|
||||
description: 'Manage keywords, question sets, and competitors around each brand.',
|
||||
},
|
||||
brandQuestionCreate: {
|
||||
title: 'New question',
|
||||
description: 'Create brand questions with the expansion tool, AI expansion, or manual input.',
|
||||
},
|
||||
tracking: {
|
||||
title: 'Data Details',
|
||||
description:
|
||||
@@ -1231,15 +1235,19 @@ const enUS = {
|
||||
},
|
||||
brands: {
|
||||
eyebrow: 'Brand Library',
|
||||
title: 'Brand Library',
|
||||
description: 'Manage keywords, question sets, and competitor assets around each brand.',
|
||||
title: 'Brand Management',
|
||||
description: 'Manage question sets and competitor assets around each brand.',
|
||||
railTitle: 'Brands',
|
||||
selectBrand: 'Pick a brand to continue managing keywords and question sets.',
|
||||
selectBrand: 'Pick a brand to continue managing question sets.',
|
||||
newBrand: 'New brand',
|
||||
editBrand: 'Edit brand',
|
||||
deleteBrand: 'Delete brand',
|
||||
guide: {
|
||||
title: 'Brand management guide',
|
||||
body: 'Manage brands, question sets, and competitor libraries for AI platform monitoring and GEO analysis.',
|
||||
},
|
||||
tabs: {
|
||||
keywords: 'Keywords',
|
||||
questions: 'Question set',
|
||||
competitors: 'Competitors',
|
||||
},
|
||||
quota: {
|
||||
@@ -1249,12 +1257,13 @@ const enUS = {
|
||||
brandsHint: 'Free defaults to 1 and paid defaults to 2.',
|
||||
keywords: 'Total keywords',
|
||||
keywordsHint: 'The total number of keywords this account can bind.',
|
||||
questions: 'Questions per keyword',
|
||||
questionsHint: 'The maximum number of questions allowed under one keyword.',
|
||||
questions: 'Account question quota',
|
||||
questionsHint: 'All brands share this account question quota.',
|
||||
},
|
||||
meta: {
|
||||
keywordCount: '{count} keywords',
|
||||
questionCount: '{count} questions',
|
||||
competitorCount: '{count} competitors',
|
||||
brandKeywordCount: '{count} keywords under this brand',
|
||||
globalKeywordUsage: 'Total keywords used {used} / {total}',
|
||||
keywordQuestionUsage: 'Questions {used} / {total}',
|
||||
@@ -1266,6 +1275,7 @@ const enUS = {
|
||||
versions: 'Version history',
|
||||
},
|
||||
actions: {
|
||||
addBrand: 'Add brand',
|
||||
newKeyword: 'New keyword',
|
||||
newQuestion: 'New question',
|
||||
newCompetitor: 'New competitor',
|
||||
@@ -1273,8 +1283,10 @@ const enUS = {
|
||||
},
|
||||
empty: {
|
||||
brands: 'No brand data yet. Create your first brand to start.',
|
||||
questions: 'No questions under this keyword yet.',
|
||||
questions: 'No questions under this brand yet.',
|
||||
competitors: 'No competitors under this brand yet.',
|
||||
brandDescription: 'No brand description yet',
|
||||
competitorDescription: 'No competitor description yet',
|
||||
},
|
||||
form: {
|
||||
brandName: 'Brand name',
|
||||
@@ -1305,7 +1317,89 @@ const enUS = {
|
||||
chooseKeyword: 'Please choose a keyword first.',
|
||||
brandLimitReached: 'Your current plan allows up to {limit} brand companies.',
|
||||
keywordLimitReached: 'This account allows up to {limit} keywords.',
|
||||
questionLimitReached: 'This keyword allows up to {limit} questions.',
|
||||
questionLimitReached: 'This account allows up to {limit} questions.',
|
||||
},
|
||||
questions: {
|
||||
create: 'New question',
|
||||
createSingle: 'Add single question',
|
||||
expand: 'Bulk expand questions',
|
||||
usage: 'Questions {used} / {total}',
|
||||
remaining: '{count} remaining',
|
||||
generate: 'Generate candidates',
|
||||
emptyCandidates: 'No candidates yet. Generate or paste questions above.',
|
||||
table: {
|
||||
question: 'Question set',
|
||||
},
|
||||
modal: {
|
||||
title: 'New question',
|
||||
currentBrand: 'Current brand',
|
||||
methods: {
|
||||
single: 'Single',
|
||||
combination: 'Expansion tool',
|
||||
aiDistill: 'AI expansion',
|
||||
manualBatch: 'Manual batch',
|
||||
},
|
||||
preview: 'Candidate preview',
|
||||
summary:
|
||||
'{total} candidates · {selected} selected · {tooShort} too short · {duplicate} duplicates',
|
||||
save: 'Save questions ({count})',
|
||||
},
|
||||
combination: {
|
||||
region: '1. Region',
|
||||
prefix: '2. Prefix',
|
||||
core: '3. Core (required)',
|
||||
industry: '4. Industry (required)',
|
||||
suffix: '5. Suffix',
|
||||
defaultWord: 'Default',
|
||||
estimate: '{count} candidates estimated',
|
||||
coreRequired: 'Core is required',
|
||||
industryRequired: 'Industry is required',
|
||||
},
|
||||
ai: {
|
||||
note: 'AI expansion consumes 1 AI point. Failed generations are refunded automatically.',
|
||||
seedTopic: 'Seed topic',
|
||||
generate: 'Generate with AI',
|
||||
},
|
||||
batch: {
|
||||
label: 'One question per line',
|
||||
},
|
||||
badges: {
|
||||
duplicate: 'Duplicate',
|
||||
tooShort: 'Too short',
|
||||
},
|
||||
messages: {
|
||||
partialSaved: 'Saved {created}; skipped {skipped}.',
|
||||
limitReached: 'This account allows up to {limit} questions shared across all brands.',
|
||||
truncated: 'Candidates exceeded the limit and were truncated to 500.',
|
||||
cacheHit: 'Used the most recent AI expansion result.',
|
||||
aiCharged: 'This AI expansion consumed {count} AI point.',
|
||||
},
|
||||
errors: {
|
||||
emptyInput: 'Enter question content first.',
|
||||
noSelection: 'Select at least one candidate.',
|
||||
noValidQuestions: 'No valid questions can be saved. Adjust candidates and try again.',
|
||||
invalidCandidate:
|
||||
'This candidate cannot be selected until it is no longer too short, duplicated, or invalid.',
|
||||
},
|
||||
page: {
|
||||
title: 'New question',
|
||||
subtitle: 'Generate candidates, review them, then save them to this brand question set.',
|
||||
steps: {
|
||||
configure: 'Configure',
|
||||
preview: 'Review candidates',
|
||||
},
|
||||
methodTitle: 'Choose a creation method',
|
||||
methodHint:
|
||||
'Candidates remain temporary until saved, so you can generate, edit, and select first.',
|
||||
methods: {
|
||||
combination: 'Expand candidates from the column-based word tool.',
|
||||
ai: 'Let AI generate candidates around a seed topic.',
|
||||
batch: 'Paste questions from a sheet or document.',
|
||||
},
|
||||
expansionHint:
|
||||
'One word per line. Empty optional columns are skipped; core and industry are required.',
|
||||
batchHint: 'One question per line. You can edit and select each one before saving.',
|
||||
},
|
||||
},
|
||||
},
|
||||
custom: {
|
||||
|
||||
Reference in New Issue
Block a user