fix(brand-questions): surface seed topic min length as actionable error
Reject AI seed topics shorter than 2 characters on both client and server with a dedicated invalid_seed_topic error code, and render an inline hint listing the offending terms so users know exactly what to fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1519,6 +1519,10 @@ const enUS = {
|
||||
},
|
||||
errors: {
|
||||
emptyInput: 'Enter search term content first.',
|
||||
aiSeedTopicTooShort:
|
||||
'{terms} is too short. Enter a user search term with at least 2 characters, such as "mattress recommendation" or "how to choose a kids bed".',
|
||||
aiSeedTopicTooShortWithMore:
|
||||
'{terms} and {count} more search terms are too short. Enter user search terms with at least 2 characters, such as "mattress recommendation" or "how to choose a kids bed".',
|
||||
noSelection: 'Select at least one candidate.',
|
||||
noValidQuestions: 'No valid search terms can be saved. Adjust candidates and try again.',
|
||||
},
|
||||
|
||||
@@ -1435,6 +1435,8 @@ const zhCN = {
|
||||
},
|
||||
errors: {
|
||||
emptyInput: "请先输入搜索词内容",
|
||||
aiSeedTopicTooShort: "{terms} 太短,请输入至少 2 个字的用户搜索词,例如“床垫推荐”或“儿童床怎么选”。",
|
||||
aiSeedTopicTooShortWithMore: "{terms} 等 {count} 个搜索词太短,请输入至少 2 个字的用户搜索词,例如“床垫推荐”或“儿童床怎么选”。",
|
||||
noSelection: "请至少选择 1 条候选",
|
||||
noValidQuestions: "没有可保存的搜索词,请调整候选后重试",
|
||||
},
|
||||
|
||||
@@ -140,6 +140,8 @@ export function isHandledAuthError(error: unknown): boolean {
|
||||
}
|
||||
|
||||
const TIMEOUT_MESSAGE_PATTERN = /^timeout of \d+ms exceeded$/i
|
||||
const VALIDATION_MIN_SEED_TOPIC_PATTERN =
|
||||
/QuestionDistillRequest\.SeedTopic|seed_topic.*min|SeedTopic.*min/i
|
||||
|
||||
function translateRawErrorMessage(raw: string): string | null {
|
||||
const mapped = errorMessageMap[raw]
|
||||
@@ -158,6 +160,13 @@ export function formatError(error: unknown): string {
|
||||
}
|
||||
|
||||
if (error instanceof ApiClientError) {
|
||||
if (
|
||||
error.message === 'invalid_seed_topic' ||
|
||||
(error.message === 'invalid_params' &&
|
||||
VALIDATION_MIN_SEED_TOPIC_PATTERN.test(error.detail ?? ''))
|
||||
) {
|
||||
return '用户搜索词太短,请输入至少 2 个字的搜索词'
|
||||
}
|
||||
const translated = translateRawErrorMessage(error.message)
|
||||
const message = translated ?? error.message.replaceAll('_', ' ')
|
||||
if (isAiPointsInsufficient(error)) {
|
||||
|
||||
@@ -30,6 +30,8 @@ interface CandidateRow extends QuestionCandidate {
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
const AI_SEED_TOPIC_MIN_LENGTH = 2
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -44,6 +46,7 @@ const aiCandidateModalOpen = ref(false)
|
||||
const aiCandidateProgress = ref(12)
|
||||
const aiCandidateStageIndex = ref(0)
|
||||
const aiWaitingKind = ref<'candidate' | 'fill'>('candidate')
|
||||
const aiSeedTopicError = ref('')
|
||||
|
||||
let aiCandidateProgressTimer: number | null = null
|
||||
let aiCandidateCloseTimer: number | null = null
|
||||
@@ -269,7 +272,7 @@ const previewMutations = {
|
||||
currentStep.value = 1
|
||||
},
|
||||
onError: (error) => {
|
||||
pageNotice.value = formatError(error)
|
||||
handleAiPreviewError(error)
|
||||
},
|
||||
}),
|
||||
}
|
||||
@@ -366,7 +369,14 @@ const normalizedAiSeedTopics = computed(() => {
|
||||
return topics
|
||||
})
|
||||
|
||||
const tooShortAiSeedTopics = computed(() =>
|
||||
normalizedAiSeedTopics.value.filter(
|
||||
(topic) => countSearchTermChars(topic) < AI_SEED_TOPIC_MIN_LENGTH,
|
||||
),
|
||||
)
|
||||
|
||||
function updateAiSeedTopics(value: string[]): void {
|
||||
clearAiSeedTopicError()
|
||||
aiForm.seed_topics.splice(0, aiForm.seed_topics.length, ...dedupeTopicParts(value))
|
||||
}
|
||||
|
||||
@@ -506,6 +516,10 @@ function normalizeSearchTermText(value: string): string {
|
||||
.replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
function countSearchTermChars(value: string): number {
|
||||
return Array.from(normalizeSearchTermText(value)).length
|
||||
}
|
||||
|
||||
function searchTermLooksValid(value: string): boolean {
|
||||
const text = normalizeSearchTermText(value)
|
||||
if (text.length < 4) {
|
||||
@@ -565,12 +579,63 @@ function changeCreateMode(mode: CreateMode): void {
|
||||
if (createMode.value === mode) {
|
||||
return
|
||||
}
|
||||
clearAiSeedTopicError()
|
||||
createMode.value = mode
|
||||
currentStep.value = 0
|
||||
candidateRows.value = []
|
||||
pageNotice.value = ''
|
||||
}
|
||||
|
||||
function setAiSeedTopicError(value: string): void {
|
||||
aiSeedTopicError.value = value
|
||||
pageNotice.value = value
|
||||
}
|
||||
|
||||
function clearAiSeedTopicError(): void {
|
||||
if (aiSeedTopicError.value && pageNotice.value === aiSeedTopicError.value) {
|
||||
pageNotice.value = ''
|
||||
}
|
||||
aiSeedTopicError.value = ''
|
||||
}
|
||||
|
||||
function formatQuotedTerms(terms: string[]): string {
|
||||
return terms
|
||||
.slice(0, 3)
|
||||
.map((term) => `“${term}”`)
|
||||
.join('、')
|
||||
}
|
||||
|
||||
function buildAiSeedTopicTooShortMessage(terms: string[]): string {
|
||||
const visibleTerms = formatQuotedTerms(terms)
|
||||
const hiddenCount = Math.max(terms.length - 3, 0)
|
||||
if (hiddenCount > 0) {
|
||||
return t('brands.questions.errors.aiSeedTopicTooShortWithMore', {
|
||||
terms: visibleTerms,
|
||||
count: hiddenCount,
|
||||
})
|
||||
}
|
||||
return t('brands.questions.errors.aiSeedTopicTooShort', {
|
||||
terms: visibleTerms,
|
||||
})
|
||||
}
|
||||
|
||||
function isAiSeedTopicValidationError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof ApiClientError &&
|
||||
(error.message === 'invalid_seed_topic' ||
|
||||
(error.message === 'invalid_params' && /SeedTopic|seed_topic/.test(error.detail ?? '')))
|
||||
)
|
||||
}
|
||||
|
||||
function handleAiPreviewError(error: unknown): void {
|
||||
const notice = formatError(error)
|
||||
if (isAiSeedTopicValidationError(error)) {
|
||||
setAiSeedTopicError(notice)
|
||||
return
|
||||
}
|
||||
pageNotice.value = notice
|
||||
}
|
||||
|
||||
function clearAiCandidateProgressTimer(): void {
|
||||
if (aiCandidateProgressTimer === null) {
|
||||
return
|
||||
@@ -629,6 +694,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
async function generateCandidates(): Promise<void> {
|
||||
pageNotice.value = ''
|
||||
clearAiSeedTopicError()
|
||||
if (questionLimitReached.value) {
|
||||
pageNotice.value = t('brands.questions.messages.limitReached', {
|
||||
limit: maxQuestions.value,
|
||||
@@ -662,6 +728,10 @@ async function generateCandidates(): Promise<void> {
|
||||
pageNotice.value = t('brands.questions.errors.emptyInput')
|
||||
return
|
||||
}
|
||||
if (tooShortAiSeedTopics.value.length) {
|
||||
setAiSeedTopicError(buildAiSeedTopicTooShortMessage(tooShortAiSeedTopics.value))
|
||||
return
|
||||
}
|
||||
beginAiCandidateTask('candidate')
|
||||
try {
|
||||
await previewMutations.ai.mutateAsync()
|
||||
@@ -947,11 +1017,15 @@ function backToBrands(): void {
|
||||
class="seed-topic-select"
|
||||
mode="tags"
|
||||
:value="aiForm.seed_topics"
|
||||
:status="aiSeedTopicError ? 'error' : undefined"
|
||||
:placeholder="t('brands.questions.ai.seedTopicPlaceholder')"
|
||||
:token-separators="aiTopicTokenSeparators"
|
||||
:options="[]"
|
||||
@change="(value: string[]) => updateAiSeedTopics(value)"
|
||||
/>
|
||||
<p v-if="aiSeedTopicError" class="field-item__error">
|
||||
{{ aiSeedTopicError }}
|
||||
</p>
|
||||
<p class="field-item__hint">
|
||||
{{ t('brands.questions.ai.seedTopicHint') }}
|
||||
</p>
|
||||
@@ -1284,6 +1358,13 @@ function backToBrands(): void {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.field-item__error {
|
||||
margin: 0;
|
||||
color: #b42318;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.seed-topic-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user