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:
@@ -30,13 +30,15 @@ type AssistCompetitor struct {
|
||||
}
|
||||
|
||||
type AnalyzeTaskRequest struct {
|
||||
Locale string `json:"locale"`
|
||||
BrandID *int64 `json:"brand_id,omitempty"`
|
||||
BrandName string `json:"brand_name"`
|
||||
Website string `json:"website,omitempty"`
|
||||
InputParams map[string]interface{} `json:"input_params,omitempty"`
|
||||
ExistingKeywords []string `json:"existing_keywords,omitempty"`
|
||||
ExistingCompetitors []AssistCompetitor `json:"existing_competitors,omitempty"`
|
||||
Locale string `json:"locale"`
|
||||
BrandID *int64 `json:"brand_id,omitempty"`
|
||||
BrandName string `json:"brand_name"`
|
||||
Website string `json:"website,omitempty"`
|
||||
BrandQuestion string `json:"brand_question,omitempty"`
|
||||
SupplementalQuestions []string `json:"supplemental_questions,omitempty"`
|
||||
InputParams map[string]interface{} `json:"input_params,omitempty"`
|
||||
ExistingKeywords []string `json:"existing_keywords,omitempty"`
|
||||
ExistingCompetitors []AssistCompetitor `json:"existing_competitors,omitempty"`
|
||||
}
|
||||
|
||||
type AnalyzeTaskResult struct {
|
||||
@@ -46,28 +48,32 @@ type AnalyzeTaskResult struct {
|
||||
}
|
||||
|
||||
type TitleTaskRequest struct {
|
||||
Locale string `json:"locale"`
|
||||
BrandID *int64 `json:"brand_id,omitempty"`
|
||||
BrandName string `json:"brand_name"`
|
||||
Website string `json:"website,omitempty"`
|
||||
BrandSummary string `json:"brand_summary,omitempty"`
|
||||
InputParams map[string]interface{} `json:"input_params,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Competitors []AssistCompetitor `json:"competitors,omitempty"`
|
||||
Locale string `json:"locale"`
|
||||
BrandID *int64 `json:"brand_id,omitempty"`
|
||||
BrandName string `json:"brand_name"`
|
||||
Website string `json:"website,omitempty"`
|
||||
BrandSummary string `json:"brand_summary,omitempty"`
|
||||
BrandQuestion string `json:"brand_question,omitempty"`
|
||||
SupplementalQuestions []string `json:"supplemental_questions,omitempty"`
|
||||
InputParams map[string]interface{} `json:"input_params,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Competitors []AssistCompetitor `json:"competitors,omitempty"`
|
||||
}
|
||||
|
||||
type OutlineTaskRequest struct {
|
||||
Locale string `json:"locale"`
|
||||
BrandID *int64 `json:"brand_id,omitempty"`
|
||||
BrandName string `json:"brand_name"`
|
||||
Website string `json:"website,omitempty"`
|
||||
BrandSummary string `json:"brand_summary,omitempty"`
|
||||
Title string `json:"title"`
|
||||
InputParams map[string]interface{} `json:"input_params,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Competitors []AssistCompetitor `json:"competitors,omitempty"`
|
||||
OutlineSections []string `json:"outline_sections,omitempty"`
|
||||
KeyPoints string `json:"key_points,omitempty"`
|
||||
Locale string `json:"locale"`
|
||||
BrandID *int64 `json:"brand_id,omitempty"`
|
||||
BrandName string `json:"brand_name"`
|
||||
Website string `json:"website,omitempty"`
|
||||
BrandSummary string `json:"brand_summary,omitempty"`
|
||||
Title string `json:"title"`
|
||||
BrandQuestion string `json:"brand_question,omitempty"`
|
||||
SupplementalQuestions []string `json:"supplemental_questions,omitempty"`
|
||||
InputParams map[string]interface{} `json:"input_params,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
Competitors []AssistCompetitor `json:"competitors,omitempty"`
|
||||
OutlineSections []string `json:"outline_sections,omitempty"`
|
||||
KeyPoints string `json:"key_points,omitempty"`
|
||||
}
|
||||
|
||||
type OutlineNode struct {
|
||||
@@ -632,6 +638,8 @@ func normalizeAnalyzeTaskRequest(req AnalyzeTaskRequest) AnalyzeTaskRequest {
|
||||
}
|
||||
req.BrandName = strings.TrimSpace(req.BrandName)
|
||||
req.Website = strings.TrimSpace(req.Website)
|
||||
req.BrandQuestion = strings.TrimSpace(req.BrandQuestion)
|
||||
req.SupplementalQuestions = []string{}
|
||||
req.ExistingKeywords = normalizeStringList(req.ExistingKeywords, 8)
|
||||
req.ExistingCompetitors = normalizeAssistCompetitors(req.ExistingCompetitors, 8)
|
||||
req.InputParams = normalizeAssistInputParams(req.InputParams)
|
||||
@@ -646,7 +654,15 @@ func normalizeTitleTaskRequest(req TitleTaskRequest) TitleTaskRequest {
|
||||
req.BrandName = strings.TrimSpace(req.BrandName)
|
||||
req.Website = strings.TrimSpace(req.Website)
|
||||
req.BrandSummary = strings.TrimSpace(req.BrandSummary)
|
||||
req.BrandQuestion = strings.TrimSpace(req.BrandQuestion)
|
||||
req.SupplementalQuestions = normalizeStringList(req.SupplementalQuestions, 3)
|
||||
req.Keywords = normalizeStringList(req.Keywords, 12)
|
||||
if req.BrandQuestion != "" {
|
||||
req.SupplementalQuestions = []string{}
|
||||
req.Keywords = []string{req.BrandQuestion}
|
||||
} else if len(req.Keywords) == 0 {
|
||||
req.Keywords = buildQuestionKeywordContext(req.BrandQuestion, req.SupplementalQuestions, 12)
|
||||
}
|
||||
req.Competitors = normalizeAssistCompetitors(req.Competitors, 8)
|
||||
req.InputParams = normalizeAssistInputParams(req.InputParams)
|
||||
return req
|
||||
@@ -662,13 +678,22 @@ func normalizeOutlineTaskRequest(req OutlineTaskRequest) OutlineTaskRequest {
|
||||
req.BrandSummary = strings.TrimSpace(req.BrandSummary)
|
||||
req.Title = strings.TrimSpace(req.Title)
|
||||
req.KeyPoints = strings.TrimSpace(req.KeyPoints)
|
||||
req.BrandQuestion = strings.TrimSpace(req.BrandQuestion)
|
||||
req.SupplementalQuestions = normalizeStringList(req.SupplementalQuestions, 3)
|
||||
req.Keywords = normalizeStringList(req.Keywords, 12)
|
||||
if len(req.Keywords) == 0 {
|
||||
req.Keywords = buildQuestionKeywordContext(req.BrandQuestion, req.SupplementalQuestions, 12)
|
||||
}
|
||||
req.Competitors = normalizeAssistCompetitors(req.Competitors, 8)
|
||||
req.OutlineSections = normalizeStringList(req.OutlineSections, 24)
|
||||
req.InputParams = normalizeAssistInputParams(req.InputParams)
|
||||
return req
|
||||
}
|
||||
|
||||
func buildQuestionKeywordContext(primaryQuestion string, supplementalQuestions []string, max int) []string {
|
||||
return normalizeStringList(append([]string{primaryQuestion}, supplementalQuestions...), max)
|
||||
}
|
||||
|
||||
func normalizeAssistInputParams(params map[string]interface{}) map[string]interface{} {
|
||||
if params == nil {
|
||||
return map[string]interface{}{}
|
||||
@@ -682,7 +707,10 @@ func normalizeAssistInputParams(params map[string]interface{}) map[string]interf
|
||||
}
|
||||
|
||||
func hasAnalyzeContext(req AnalyzeTaskRequest) bool {
|
||||
if req.BrandName != "" || req.Website != "" {
|
||||
if req.BrandName != "" || req.Website != "" || req.BrandQuestion != "" {
|
||||
return true
|
||||
}
|
||||
if len(req.SupplementalQuestions) > 0 {
|
||||
return true
|
||||
}
|
||||
for _, value := range req.InputParams {
|
||||
@@ -707,7 +735,10 @@ func hasAnalyzeContext(req AnalyzeTaskRequest) bool {
|
||||
}
|
||||
|
||||
func hasTitleContext(req TitleTaskRequest) bool {
|
||||
if req.BrandName != "" || req.Website != "" || req.BrandSummary != "" {
|
||||
if req.BrandName != "" || req.Website != "" || req.BrandSummary != "" || req.BrandQuestion != "" {
|
||||
return true
|
||||
}
|
||||
if len(req.SupplementalQuestions) > 0 {
|
||||
return true
|
||||
}
|
||||
if len(req.Keywords) > 0 || len(req.Competitors) > 0 {
|
||||
@@ -738,7 +769,10 @@ func hasOutlineContext(req OutlineTaskRequest) bool {
|
||||
if req.Title == "" || len(req.OutlineSections) == 0 {
|
||||
return false
|
||||
}
|
||||
if req.BrandName != "" || req.Website != "" || req.BrandSummary != "" || req.KeyPoints != "" {
|
||||
if req.BrandName != "" || req.Website != "" || req.BrandSummary != "" || req.KeyPoints != "" || req.BrandQuestion != "" {
|
||||
return true
|
||||
}
|
||||
if len(req.SupplementalQuestions) > 0 {
|
||||
return true
|
||||
}
|
||||
if len(req.Keywords) > 0 || len(req.Competitors) > 0 {
|
||||
@@ -835,13 +869,18 @@ func buildOutlineAIPointMeteredText(req OutlineTaskRequest) string {
|
||||
}
|
||||
|
||||
func analyzePromptParams(templateKey, templateName string, req AnalyzeTaskRequest) map[string]interface{} {
|
||||
keywords := buildQuestionKeywordContext(req.BrandQuestion, nil, 12)
|
||||
return map[string]interface{}{
|
||||
"template_key": templateKey,
|
||||
"template_name": templateName,
|
||||
"locale": req.Locale,
|
||||
"brand_name": req.BrandName,
|
||||
"official_website": req.Website,
|
||||
"brand_question": req.BrandQuestion,
|
||||
"primary_question": req.BrandQuestion,
|
||||
"input_params": req.InputParams,
|
||||
"keywords": keywords,
|
||||
"primary_keyword": req.BrandQuestion,
|
||||
"existing_keywords": req.ExistingKeywords,
|
||||
"existing_competitors": req.ExistingCompetitors,
|
||||
}
|
||||
@@ -855,6 +894,8 @@ func titlePromptParams(templateKey, templateName string, req TitleTaskRequest) m
|
||||
"brand_name": req.BrandName,
|
||||
"official_website": req.Website,
|
||||
"brand_summary": req.BrandSummary,
|
||||
"brand_question": req.BrandQuestion,
|
||||
"primary_question": req.BrandQuestion,
|
||||
"input_params": req.InputParams,
|
||||
"keywords": req.Keywords,
|
||||
"competitors": req.Competitors,
|
||||
@@ -863,7 +904,10 @@ func titlePromptParams(templateKey, templateName string, req TitleTaskRequest) m
|
||||
}
|
||||
mergePromptParams(params, req.InputParams)
|
||||
|
||||
if len(req.Keywords) > 0 {
|
||||
if req.BrandQuestion != "" {
|
||||
params["primary_keyword"] = req.BrandQuestion
|
||||
params["keyword_count"] = 1
|
||||
} else if len(req.Keywords) > 0 {
|
||||
params["primary_keyword"] = req.Keywords[0]
|
||||
params["keyword_count"] = len(req.Keywords)
|
||||
}
|
||||
@@ -892,24 +936,34 @@ func titlePromptParams(templateKey, templateName string, req TitleTaskRequest) m
|
||||
|
||||
func outlinePromptParams(templateKey, templateName string, req OutlineTaskRequest) map[string]interface{} {
|
||||
params := map[string]interface{}{
|
||||
"template_key": templateKey,
|
||||
"template_name": templateName,
|
||||
"locale": req.Locale,
|
||||
"title": req.Title,
|
||||
"brand_name": req.BrandName,
|
||||
"official_website": req.Website,
|
||||
"brand_summary": req.BrandSummary,
|
||||
"input_params": req.InputParams,
|
||||
"keywords": req.Keywords,
|
||||
"competitors": req.Competitors,
|
||||
"competitor_count": len(req.Competitors),
|
||||
"outline_sections": req.OutlineSections,
|
||||
"key_points": req.KeyPoints,
|
||||
"current_year": time.Now().Year(),
|
||||
"template_key": templateKey,
|
||||
"template_name": templateName,
|
||||
"locale": req.Locale,
|
||||
"title": req.Title,
|
||||
"brand_name": req.BrandName,
|
||||
"official_website": req.Website,
|
||||
"brand_summary": req.BrandSummary,
|
||||
"brand_question": req.BrandQuestion,
|
||||
"primary_question": req.BrandQuestion,
|
||||
"supplemental_questions": req.SupplementalQuestions,
|
||||
"input_params": req.InputParams,
|
||||
"keywords": req.Keywords,
|
||||
"competitors": req.Competitors,
|
||||
"competitor_count": len(req.Competitors),
|
||||
"outline_sections": req.OutlineSections,
|
||||
"key_points": req.KeyPoints,
|
||||
"current_year": time.Now().Year(),
|
||||
}
|
||||
mergePromptParams(params, req.InputParams)
|
||||
|
||||
if len(req.Keywords) > 0 {
|
||||
if req.BrandQuestion != "" {
|
||||
params["primary_keyword"] = req.BrandQuestion
|
||||
params["keyword_count"] = len(buildQuestionKeywordContext(
|
||||
req.BrandQuestion,
|
||||
req.SupplementalQuestions,
|
||||
12,
|
||||
))
|
||||
} else if len(req.Keywords) > 0 {
|
||||
params["primary_keyword"] = req.Keywords[0]
|
||||
params["keyword_count"] = len(req.Keywords)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user