feat(imitation): simplify form fields and make brand_name required
Remove industry, target_audience, content_goal, tone, and length_goal from the imitation form; brand_name is now mandatory with autocomplete from brand library, keywords are pre-populated from the selected brand's search keywords, and output length is hardcoded to ~2000 characters in the prompt. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -723,14 +723,9 @@ const enUS = {
|
||||
sourceURL: 'Article URL',
|
||||
sourceTitle: 'Source title',
|
||||
language: 'Article language',
|
||||
industry: 'Industry / scenario',
|
||||
brandName: 'Brand / subject',
|
||||
region: 'Region',
|
||||
targetAudience: 'Target audience',
|
||||
contentGoal: 'Content goal',
|
||||
tone: 'Tone',
|
||||
lengthGoal: 'Length',
|
||||
keywords: 'Keywords',
|
||||
keywords: 'Optimization keywords',
|
||||
preservePoints: 'Keep / emphasize',
|
||||
avoidPoints: 'Avoid',
|
||||
extraRequirements: 'Extra requirements',
|
||||
@@ -743,21 +738,17 @@ const enUS = {
|
||||
'Selected groups are retrieved in the backend and passed to generation as reference context.',
|
||||
sourceURLPlaceholder: 'https://example.com/article',
|
||||
sourceTitlePlaceholder: 'Optional. Citation titles are prefilled automatically.',
|
||||
industryPlaceholder: 'For example: home improvement, education, SaaS, healthcare',
|
||||
brandNamePlaceholder: 'Optional. Helps the article fit a specific brand or subject.',
|
||||
brandNamePlaceholder: 'Required. Type the target subject, or choose from the brand library.',
|
||||
regionPlaceholder: 'For example: Shanghai, East China, nationwide, North America',
|
||||
targetAudiencePlaceholder:
|
||||
'For example: first-time buyers, channel partners, procurement leads',
|
||||
contentGoalPlaceholder: 'For example: education, conversion, brand trust, selection advice',
|
||||
tonePlaceholder: 'For example: professional, conversational, comparative, news analysis',
|
||||
lengthGoalPlaceholder: 'For example: around 1200 words, short post, long-form guide',
|
||||
keywordsPlaceholder: 'Type keywords and press Enter',
|
||||
keywordsPlaceholder:
|
||||
'Type optimization keywords and press Enter. Brand-library options appear after choosing a subject.',
|
||||
preservePointsPlaceholder: 'List facts, data, arguments, or examples that must be kept.',
|
||||
avoidPointsPlaceholder: 'List claims, competitors, compliance risks, or wording to avoid.',
|
||||
extraRequirementsPlaceholder:
|
||||
'Add channel, structure, title style, or industry-specific instructions.',
|
||||
queued: 'Rewrite task submitted and queued.',
|
||||
sourceURLRequired: 'Enter the source article URL first.',
|
||||
brandNameRequired: 'Enter the brand / subject first.',
|
||||
submitError: 'Failed to submit rewrite task.',
|
||||
backToList: 'Back to rewrites',
|
||||
},
|
||||
|
||||
@@ -691,14 +691,9 @@ const zhCN = {
|
||||
sourceURL: "文章 URL",
|
||||
sourceTitle: "来源标题",
|
||||
language: "文章语言",
|
||||
industry: "行业/场景",
|
||||
brandName: "品牌/主体",
|
||||
region: "地域",
|
||||
targetAudience: "目标读者",
|
||||
contentGoal: "内容目标",
|
||||
tone: "语气风格",
|
||||
lengthGoal: "篇幅要求",
|
||||
keywords: "关键词",
|
||||
keywords: "优化关键词",
|
||||
preservePoints: "必须保留/强调",
|
||||
avoidPoints: "需要规避",
|
||||
extraRequirements: "其他要求",
|
||||
@@ -709,19 +704,15 @@ const zhCN = {
|
||||
knowledgeBaseHint: "选择后,后台会检索相关知识片段并作为生成参考,未选择则只基于来源文章和当前设置生成。",
|
||||
sourceURLPlaceholder: "https://example.com/article",
|
||||
sourceTitlePlaceholder: "可选,引用来源标题会自动带入",
|
||||
industryPlaceholder: "例如:家居建材、教育培训、SaaS、医疗健康",
|
||||
brandNamePlaceholder: "可选,填写后文章会自然贴合该品牌或主体",
|
||||
brandNamePlaceholder: "必填,可输入目标达成词,或从品牌库选择",
|
||||
regionPlaceholder: "例如:上海、华东、全国、北美市场",
|
||||
targetAudiencePlaceholder: "例如:首次购买用户、渠道代理、企业采购负责人",
|
||||
contentGoalPlaceholder: "例如:科普说明、转化种草、品牌信任、选型建议",
|
||||
tonePlaceholder: "例如:专业可信、轻松口语、评测对比、新闻解读",
|
||||
lengthGoalPlaceholder: "例如:1200字左右、短文、深度长文",
|
||||
keywordsPlaceholder: "输入关键词后回车,可多选",
|
||||
keywordsPlaceholder: "输入优化关键词后回车,可多选;选择品牌库主体后会显示搜索词候选",
|
||||
preservePointsPlaceholder: "列出必须保留的事实、数据、观点或案例",
|
||||
avoidPointsPlaceholder: "列出不能出现的说法、竞品、合规禁区或表达方式",
|
||||
extraRequirementsPlaceholder: "补充行业、渠道、结构、标题风格等自由要求",
|
||||
queued: "仿写任务已提交,正在排队生成",
|
||||
sourceURLRequired: "请先填写来源文章 URL",
|
||||
brandNameRequired: "请先填写品牌/主体",
|
||||
submitError: "仿写任务提交失败",
|
||||
backToList: "返回仿写列表",
|
||||
},
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { LeftOutlined, LinkOutlined } from '@ant-design/icons-vue'
|
||||
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import KnowledgeGroupSelect from '@/components/KnowledgeGroupSelect.vue'
|
||||
import { articlesApi } from '@/lib/api'
|
||||
import { articlesApi, brandsApi } from '@/lib/api'
|
||||
import { formatError } from '@/lib/errors'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -19,13 +19,8 @@ const form = reactive({
|
||||
source_url: '',
|
||||
source_title: '',
|
||||
locale: 'zh-CN',
|
||||
industry: '',
|
||||
brand_name: '',
|
||||
region: '',
|
||||
target_audience: '',
|
||||
content_goal: '',
|
||||
tone: '',
|
||||
length_goal: '',
|
||||
keywords: [] as string[],
|
||||
preserve_points: '',
|
||||
avoid_points: '',
|
||||
@@ -34,8 +29,40 @@ const form = reactive({
|
||||
knowledge_group_ids: [] as number[],
|
||||
})
|
||||
|
||||
const brandsQuery = useQuery({
|
||||
queryKey: ['brands'],
|
||||
queryFn: () => brandsApi.list(),
|
||||
})
|
||||
|
||||
const brandNameOptions = computed(() =>
|
||||
(brandsQuery.data.value ?? []).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
})),
|
||||
)
|
||||
|
||||
const selectedBrand = computed(
|
||||
() => brandsQuery.data.value?.find((item) => item.name === form.brand_name.trim()) ?? null,
|
||||
)
|
||||
|
||||
const questionsQuery = useQuery({
|
||||
queryKey: computed(() => ['brands', selectedBrand.value?.id, 'questions', 'imitation']),
|
||||
enabled: computed(() => Boolean(selectedBrand.value?.id)),
|
||||
queryFn: () => brandsApi.listQuestions(selectedBrand.value?.id as number),
|
||||
})
|
||||
|
||||
const keywordOptions = computed(() =>
|
||||
(questionsQuery.data.value ?? []).map((item) => ({
|
||||
label: item.question_text,
|
||||
value: item.question_text,
|
||||
})),
|
||||
)
|
||||
|
||||
const canSubmit = computed(
|
||||
() => form.source_url.trim().length > 0 && !generateMutation.isPending.value,
|
||||
() =>
|
||||
form.source_url.trim().length > 0 &&
|
||||
form.brand_name.trim().length > 0 &&
|
||||
!generateMutation.isPending.value,
|
||||
)
|
||||
|
||||
const generateMutation = useMutation({
|
||||
@@ -44,13 +71,8 @@ const generateMutation = useMutation({
|
||||
source_url: form.source_url.trim(),
|
||||
source_title: form.source_title.trim(),
|
||||
locale: form.locale,
|
||||
industry: form.industry.trim(),
|
||||
brand_name: form.brand_name.trim(),
|
||||
region: form.region.trim(),
|
||||
target_audience: form.target_audience.trim(),
|
||||
content_goal: form.content_goal.trim(),
|
||||
tone: form.tone.trim(),
|
||||
length_goal: form.length_goal.trim(),
|
||||
keywords: form.keywords.map((item) => item.trim()).filter(Boolean),
|
||||
preserve_points: form.preserve_points.trim(),
|
||||
avoid_points: form.avoid_points.trim(),
|
||||
@@ -95,6 +117,10 @@ function submit(): void {
|
||||
message.warning(t('imitation.create.sourceURLRequired'))
|
||||
return
|
||||
}
|
||||
if (!form.brand_name.trim()) {
|
||||
message.warning(t('imitation.create.brandNameRequired'))
|
||||
return
|
||||
}
|
||||
generateMutation.mutate()
|
||||
}
|
||||
</script>
|
||||
@@ -204,17 +230,11 @@ function submit(): void {
|
||||
|
||||
<div class="imitation-generate-page__field-grid">
|
||||
<div class="imitation-generate-page__field">
|
||||
<label>{{ t('imitation.create.industry') }}</label>
|
||||
<a-input
|
||||
v-model:value="form.industry"
|
||||
:placeholder="t('imitation.create.industryPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="imitation-generate-page__field">
|
||||
<label>{{ t('imitation.create.brandName') }}</label>
|
||||
<a-input
|
||||
<label class="required-asterisk">{{ t('imitation.create.brandName') }}</label>
|
||||
<a-auto-complete
|
||||
v-model:value="form.brand_name"
|
||||
allow-clear
|
||||
:options="brandNameOptions"
|
||||
:placeholder="t('imitation.create.brandNamePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
@@ -227,44 +247,13 @@ function submit(): void {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="imitation-generate-page__field">
|
||||
<label>{{ t('imitation.create.targetAudience') }}</label>
|
||||
<a-input
|
||||
v-model:value="form.target_audience"
|
||||
:placeholder="t('imitation.create.targetAudiencePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="imitation-generate-page__field">
|
||||
<label>{{ t('imitation.create.contentGoal') }}</label>
|
||||
<a-input
|
||||
v-model:value="form.content_goal"
|
||||
:placeholder="t('imitation.create.contentGoalPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="imitation-generate-page__field">
|
||||
<label>{{ t('imitation.create.tone') }}</label>
|
||||
<a-input
|
||||
v-model:value="form.tone"
|
||||
:placeholder="t('imitation.create.tonePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="imitation-generate-page__field">
|
||||
<label>{{ t('imitation.create.lengthGoal') }}</label>
|
||||
<a-input
|
||||
v-model:value="form.length_goal"
|
||||
:placeholder="t('imitation.create.lengthGoalPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="imitation-generate-page__field imitation-generate-page__field--wide">
|
||||
<label>{{ t('imitation.create.keywords') }}</label>
|
||||
<a-select
|
||||
v-model:value="form.keywords"
|
||||
mode="tags"
|
||||
style="width: 100%"
|
||||
:options="keywordOptions"
|
||||
:placeholder="t('imitation.create.keywordsPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -278,7 +278,8 @@ runtime:
|
||||
- 文章结构不动,文章分析核心维度不动,只动目标达成词(也就是brand_name)
|
||||
- 保留源文章中的事实信息、论证逻辑和可复用观点。
|
||||
- 不要逐句同义替换,不要复制源文的独特措辞,不要编造源文、知识库和补充要求之外的事实。
|
||||
- 如果给出了地域、品牌、目标读者或内容目标,标题和正文都要自然贴合这些上下文。
|
||||
- 如果给出了地域、品牌/主体或优化关键词,标题和正文都要自然贴合这些上下文。
|
||||
- 全文篇幅控制在 2000 字左右。
|
||||
%s
|
||||
|
||||
输出格式:Markdown。正文结构清晰,可直接进入文章编辑器。
|
||||
@@ -301,14 +302,10 @@ runtime:
|
||||
zh-CN: "输出语言:中文简体。"
|
||||
en-US: "Output language: English."
|
||||
article_imitation_setting_labels:
|
||||
industry: "行业/场景"
|
||||
brand_name: "品牌/主体"
|
||||
region: "地域"
|
||||
target_audience: "目标读者"
|
||||
content_goal: "内容目标"
|
||||
tone: "语气风格"
|
||||
length_goal: "篇幅要求"
|
||||
keywords: "关键词"
|
||||
keywords: "优化关键词"
|
||||
preserve_points: "必须保留/强调"
|
||||
avoid_points: "需要规避"
|
||||
extra_requirements: "其他要求"
|
||||
|
||||
+3
-6
@@ -278,7 +278,8 @@ runtime:
|
||||
- 文章结构不动,文章分析核心维度不动,只动目标达成词(也就是brand_name)
|
||||
- 保留源文章中的事实信息、论证逻辑和可复用观点。
|
||||
- 不要逐句同义替换,不要复制源文的独特措辞,不要编造源文、知识库和补充要求之外的事实。
|
||||
- 如果给出了地域、品牌、目标读者或内容目标,标题和正文都要自然贴合这些上下文。
|
||||
- 如果给出了地域、品牌/主体或优化关键词,标题和正文都要自然贴合这些上下文。
|
||||
- 全文篇幅控制在 2000 字左右。
|
||||
%s
|
||||
|
||||
输出格式:Markdown。正文结构清晰,可直接进入文章编辑器。
|
||||
@@ -301,14 +302,10 @@ runtime:
|
||||
zh-CN: "输出语言:中文简体。"
|
||||
en-US: "Output language: English."
|
||||
article_imitation_setting_labels:
|
||||
industry: "行业/场景"
|
||||
brand_name: "品牌/主体"
|
||||
region: "地域"
|
||||
target_audience: "目标读者"
|
||||
content_goal: "内容目标"
|
||||
tone: "语气风格"
|
||||
length_goal: "篇幅要求"
|
||||
keywords: "关键词"
|
||||
keywords: "优化关键词"
|
||||
preserve_points: "必须保留/强调"
|
||||
avoid_points: "需要规避"
|
||||
extra_requirements: "其他要求"
|
||||
|
||||
@@ -748,13 +748,8 @@ export interface GenerateImitationRequest {
|
||||
source_url: string
|
||||
source_title?: string
|
||||
locale?: string
|
||||
industry?: string
|
||||
brand_name?: string
|
||||
brand_name: string
|
||||
region?: string
|
||||
target_audience?: string
|
||||
content_goal?: string
|
||||
tone?: string
|
||||
length_goal?: string
|
||||
keywords?: string[]
|
||||
preserve_points?: string
|
||||
avoid_points?: string
|
||||
|
||||
@@ -278,7 +278,8 @@ runtime:
|
||||
- 文章结构不动,文章分析核心维度不动,只动目标达成词(也就是brand_name)
|
||||
- 保留源文章中的事实信息、论证逻辑和可复用观点。
|
||||
- 不要逐句同义替换,不要复制源文的独特措辞,不要编造源文、知识库和补充要求之外的事实。
|
||||
- 如果给出了地域、品牌、目标读者或内容目标,标题和正文都要自然贴合这些上下文。
|
||||
- 如果给出了地域、品牌/主体或优化关键词,标题和正文都要自然贴合这些上下文。
|
||||
- 全文篇幅控制在 2000 字左右。
|
||||
%s
|
||||
|
||||
输出格式:Markdown。正文结构清晰,可直接进入文章编辑器。
|
||||
@@ -301,14 +302,10 @@ runtime:
|
||||
zh-CN: "输出语言:中文简体。"
|
||||
en-US: "Output language: English."
|
||||
article_imitation_setting_labels:
|
||||
industry: "行业/场景"
|
||||
brand_name: "品牌/主体"
|
||||
region: "地域"
|
||||
target_audience: "目标读者"
|
||||
content_goal: "内容目标"
|
||||
tone: "语气风格"
|
||||
length_goal: "篇幅要求"
|
||||
keywords: "关键词"
|
||||
keywords: "优化关键词"
|
||||
preserve_points: "必须保留/强调"
|
||||
avoid_points: "需要规避"
|
||||
extra_requirements: "其他要求"
|
||||
|
||||
@@ -47,13 +47,8 @@ type GenerateImitationRequest struct {
|
||||
SourceURL string `json:"source_url" binding:"required"`
|
||||
SourceTitle string `json:"source_title"`
|
||||
Locale string `json:"locale"`
|
||||
Industry string `json:"industry"`
|
||||
BrandName string `json:"brand_name"`
|
||||
Region string `json:"region"`
|
||||
TargetAudience string `json:"target_audience"`
|
||||
ContentGoal string `json:"content_goal"`
|
||||
Tone string `json:"tone"`
|
||||
LengthGoal string `json:"length_goal"`
|
||||
Keywords []string `json:"keywords"`
|
||||
PreservePoints string `json:"preserve_points"`
|
||||
AvoidPoints string `json:"avoid_points"`
|
||||
@@ -135,6 +130,9 @@ func (s *ArticleImitationService) Generate(ctx context.Context, req GenerateImit
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(req.BrandName) == "" {
|
||||
return nil, response.ErrBadRequest(40001, "brand_name_required", "brand_name is required")
|
||||
}
|
||||
|
||||
quotaRepo := repository.NewQuotaRepository(s.pool)
|
||||
balance, err := quotaRepo.GetCurrentBalance(ctx, actor.TenantID, "article_generation")
|
||||
@@ -587,13 +585,9 @@ func buildImitationInputParams(sourceURL string, req GenerateImitationRequest) m
|
||||
"source_url": sourceURL,
|
||||
"source_title": strings.TrimSpace(req.SourceTitle),
|
||||
"locale": normalizeImitationLocale(req.Locale),
|
||||
"industry": strings.TrimSpace(req.Industry),
|
||||
"brand_name": strings.TrimSpace(req.BrandName),
|
||||
"region": strings.TrimSpace(req.Region),
|
||||
"target_audience": strings.TrimSpace(req.TargetAudience),
|
||||
"content_goal": strings.TrimSpace(req.ContentGoal),
|
||||
"tone": strings.TrimSpace(req.Tone),
|
||||
"length_goal": strings.TrimSpace(req.LengthGoal),
|
||||
"length_goal": "2000字左右",
|
||||
"keywords": normalizeImitationKeywords(req.Keywords),
|
||||
"preserve_points": strings.TrimSpace(req.PreservePoints),
|
||||
"avoid_points": strings.TrimSpace(req.AvoidPoints),
|
||||
@@ -631,12 +625,8 @@ func buildImitationGenerationPrompt(params map[string]interface{}, sourceContent
|
||||
}
|
||||
|
||||
var settings strings.Builder
|
||||
appendPromptLine(&settings, "industry", extractString(params, "industry"))
|
||||
appendPromptLine(&settings, "brand_name", extractString(params, "brand_name"))
|
||||
appendPromptLine(&settings, "region", extractString(params, "region"))
|
||||
appendPromptLine(&settings, "target_audience", extractString(params, "target_audience"))
|
||||
appendPromptLine(&settings, "content_goal", extractString(params, "content_goal"))
|
||||
appendPromptLine(&settings, "tone", extractString(params, "tone"))
|
||||
appendPromptLine(&settings, "length_goal", extractString(params, "length_goal"))
|
||||
if len(keywords) > 0 {
|
||||
appendPromptLine(&settings, "keywords", strings.Join(keywords, "、"))
|
||||
@@ -674,12 +664,8 @@ func buildImitationKnowledgeQuery(params map[string]interface{}) string {
|
||||
}
|
||||
|
||||
appendValue(extractString(params, "source_title"))
|
||||
appendValue(extractString(params, "industry"))
|
||||
appendValue(extractString(params, "brand_name"))
|
||||
appendValue(extractString(params, "region"))
|
||||
appendValue(extractString(params, "target_audience"))
|
||||
appendValue(extractString(params, "content_goal"))
|
||||
appendValue(extractString(params, "tone"))
|
||||
appendValue(strings.Join(extractStringList(params["keywords"], 16), "\n"))
|
||||
appendValue(extractString(params, "extra_requirements"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user