feat(questions): make brand questions the primary monitoring & template axis
Deployment Config CI / Deployment Config (push) Successful in 29s
Frontend CI / Frontend (push) Successful in 4m4s
Backend CI / Backend (push) Failing after 7m12s

- 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:
2026-05-13 15:59:39 +08:00
parent 37b0b32327
commit 1eae6fb6d4
36 changed files with 2119 additions and 412 deletions
+61 -16
View File
@@ -151,6 +151,8 @@ const enUS = {
template_outline_generate: 'AI Outline Generation',
kol_prompt_generate: 'KOL Prompt Generation',
kol_prompt_optimize: 'KOL Prompt Optimize',
question_distill: 'AI Candidate Question Generation',
question_combination_fill: 'Expansion Tool AI Fill',
},
aiUsageStatus: {
pending: 'Processing',
@@ -243,9 +245,20 @@ const enUS = {
aiPoints: {
balance: 'Available Points',
baseChars: 'Base Characters',
usageTypeFallback: 'AI Service Call',
ledgerTitle: 'Usage Ledger',
ledgerLimit: 'Latest {limit} records',
ledgerTotal: '{total} records',
refundedHelp:
'The system deducted points first. After the task failed, the points were returned automatically, so net usage is 0.',
amount: {
netZero: 'Net 0',
zero: '0',
pendingFlow: '{points} pts reserved',
completedFlow: '{points} pts charged',
refundedFlow: '{points} pts deducted, then returned',
failedFlow: 'No charge',
},
table: {
usageType: 'Usage',
points: 'Points',
@@ -431,10 +444,11 @@ const enUS = {
},
tracking: {
brandPlaceholder: 'Select brand',
keywordPlaceholder: 'Select keyword',
keywordPlaceholder: 'Select question set',
allQuestionSets: 'All question sets',
collectNow: 'Collect Now',
collectBrandRequired: 'Select a brand before collecting.',
collectKeywordRequired: 'Select a keyword before collecting the current question set.',
collectKeywordRequired: 'Select a question set before collecting.',
collectTodayOnly: "Collect Now only supports today's data. Switch back to today to continue.",
collectClientChecking: "Checking whether the current account's desktop client is online.",
collectClientOnlineRequired:
@@ -458,8 +472,9 @@ const enUS = {
platformMatrixTitle: 'Platform Comparison Matrix',
hotQuestions: 'Hot Questions',
hotQuestionsTitle: 'Hot Questions',
hotQuestionsHint: 'Questions come from the brand library under the selected keyword.',
questionSearchPlaceholder: 'Search questions under the selected keyword',
hotQuestionsHint:
'Choose all question sets or one specific question. Selecting one question limits display and collection to that question.',
questionSearchPlaceholder: 'Search questions in the selected scope',
questionDetailEyebrow: 'Question Detail',
questionDetailCollectedAt: 'Collected at {time}',
questionDetailFallback: 'Question Detail',
@@ -813,6 +828,9 @@ const enUS = {
website: 'Official website',
brandSummary: 'AI brand summary',
keywords: 'Keywords',
brandQuestions: 'Brand questions',
primaryQuestion: 'Primary brand question',
supplementalQuestions: 'Supplemental questions',
competitors: 'Competitors',
chooseTitle: 'Choose a title',
customTitle: 'Custom title',
@@ -829,11 +847,17 @@ const enUS = {
hints: {
brandContext: 'Optional. Selecting a brand pulls in keywords and competitor context.',
brandFlow:
'You can pick a brand from the library or type one manually. AI analysis will expand keywords, competitors, and title options.',
'You can pick a brand from the library or type one manually. After you choose a brand question, AI will prepare the summary, competitors, and title options.',
brandSummaryPlaceholder:
'After analysis, AI will summarize the brand angle, positioning, and content opportunities here.',
keywords:
'Type keywords directly or reuse brand-library keywords. AI results will be merged into this list.',
brandQuestions:
'The primary question controls the article spine; supplemental questions broaden nearby search intent.',
supplementalQuestions:
'Choose up to 3 so the article covers related intent without drifting.',
questionEmpty: 'No questions are available for this brand yet.',
selectBrandFirst: 'Choose a brand from the library first.',
competitors:
'Competitors stay editable. You can add rows manually; saving to the competitor library is only available after choosing a library brand.',
competitorEmptyEditable:
@@ -891,6 +915,8 @@ const enUS = {
brandName: 'Enter a brand name, or refine the selected library brand',
website: 'Official website, optional',
keywords: 'Type keywords and press Enter',
primaryQuestion: 'Choose one primary brand question',
supplementalQuestions: 'Optional, choose up to 3 supplemental questions',
customTitle: 'Override the AI title here if needed',
customOutline: 'Add another section, for example “Buying advice”',
keyPoints: 'Add any extra instructions, key angles, or constraints for the article',
@@ -904,32 +930,33 @@ const enUS = {
aiBanner: {
title: 'One-click AI analysis',
defaultDescription:
'AI will complete the brand summary, find core keywords, and extract competitor context.',
'AI will use the brand question to complete the brand summary and extract competitor context.',
productReviewDescription:
'AI will organize the product context, extract review keywords, and suggest comparable alternatives.',
'AI will use the brand question to organize the product context and suggest comparable alternatives.',
researchReportDescription:
'AI will organize the topic context, extract research keywords, and suggest reference targets.',
'AI will use the brand question to organize the topic context and suggest reference targets.',
},
assist: {
analyzeTitle: 'AI is analyzing keywords and competitors. Please wait…',
analyzeTitleNoCompetitors: 'AI is analyzing keywords and context. Please wait…',
analyzeTitle: 'AI is analyzing brand questions and competitors. Please wait…',
analyzeTitleNoCompetitors: 'AI is analyzing brand questions and context. Please wait…',
titleTitle: 'AI is generating titles. Please wait…',
outlineTitle: 'AI is generating the article outline. Please wait…',
analyzeStages: {
brand: 'Analyzing the brand context…',
keywords: 'Extracting keyword opportunities…',
questions: 'Organizing brand questions…',
competitors: 'Mapping competitors and websites…',
context: 'Organizing the content context…',
},
titleStages: {
context: 'Preparing the current brief…',
strategy: 'Generating title directions from keywords and competitors…',
strategyNoCompetitors: 'Generating title directions from the keywords and brief…',
strategy: 'Generating title directions from brand questions and competitors…',
strategyNoCompetitors: 'Generating title directions from the brand question and brief…',
finalize: 'Returning 5 title options…',
},
outlineStages: {
context: 'Preparing the title, keywords, and competitor context…',
contextNoCompetitors: 'Preparing the title, keywords, and content context…',
context: 'Preparing the title, brand questions, and competitor context…',
contextNoCompetitors: 'Preparing the title, brand questions, and content context…',
structure: 'Generating the article outline from the selected structure…',
finalize: 'Returning the editable outline result…',
},
@@ -937,6 +964,8 @@ const enUS = {
messages: {
requiredField: 'Please fill in {field} first.',
missingBrand: 'Choose a brand from the library or enter a brand name first.',
selectBrandBeforeQuestion: 'Choose a brand from the library before selecting a question.',
missingPrimaryQuestion: 'Choose one primary brand question.',
missingTitle: 'Please choose or enter an article title.',
missingOutline: 'Please select at least one outline section.',
missingGeneratedOutline: 'Please generate and confirm the article outline first.',
@@ -1355,10 +1384,27 @@ const enUS = {
coreRequired: 'Core is required',
industryRequired: 'Industry is required',
},
aiFill: {
button: 'AI fill',
success: 'Word columns filled. You can keep editing.',
charged: 'Word columns filled. This used {count} AI point.',
waitingTitle: 'AI is filling the expansion tool. Please wait...',
waitingStages: {
region: 'Detecting city, province capital, and macro region...',
generate: 'Analyzing brand context and high-frequency intent...',
refine: 'Refining short combinable keyword groups...',
},
},
ai: {
note: 'AI expansion consumes 1 AI point. Failed generations are refunded automatically.',
seedTopic: 'Seed topic',
generate: 'Generate with AI',
waitingTitle: 'AI is generating candidate questions. Please wait...',
waitingStages: {
context: 'Organizing brand and topic context...',
generate: 'Generating high-intent candidate questions...',
refine: 'Deduplicating and filtering savable questions...',
},
},
batch: {
label: 'One question per line',
@@ -1370,6 +1416,7 @@ const enUS = {
messages: {
partialSaved: 'Saved {created}; skipped {skipped}.',
limitReached: 'This account allows up to {limit} questions shared across all brands.',
selectionLimitReached: 'This plan allows up to {limit} questions. Selection limit reached.',
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.',
@@ -1378,8 +1425,6 @@ const enUS = {
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',