Skip technical form fill on AI product create.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
a1e73d1c9e
commit
f8b865b0aa
@@ -5,7 +5,6 @@ import {
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { ContentStatus, TechnicalFieldType } from '@prisma/client';
|
import { ContentStatus, TechnicalFieldType } from '@prisma/client';
|
||||||
import { AuthUser } from '../auth/auth.types';
|
import { AuthUser } from '../auth/auth.types';
|
||||||
import { CategoryTechnicalFormService } from '../categories/category-technical-form.service';
|
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import {
|
import {
|
||||||
CreateProductByAiDto,
|
CreateProductByAiDto,
|
||||||
@@ -37,7 +36,6 @@ type AiProductDraft = {
|
|||||||
summary: string;
|
summary: string;
|
||||||
descriptionHtml: string;
|
descriptionHtml: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
technicalValues: Record<string, string | string[]>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -47,7 +45,6 @@ export class ProductAiService {
|
|||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly products: ProductsService,
|
private readonly products: ProductsService,
|
||||||
private readonly technicalInfo: ProductTechnicalInfoService,
|
private readonly technicalInfo: ProductTechnicalInfoService,
|
||||||
private readonly technicalFormService: CategoryTechnicalFormService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createFromAi(
|
async createFromAi(
|
||||||
@@ -70,16 +67,10 @@ export class ProductAiService {
|
|||||||
throw new BadRequestException('Product category not found');
|
throw new BadRequestException('Product category not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = await this.technicalFormService.getFormForCategory(
|
|
||||||
businessId,
|
|
||||||
categoryId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const draft = await this.generateDraft({
|
const draft = await this.generateDraft({
|
||||||
categoryName: category.name,
|
categoryName: category.name,
|
||||||
productName: dto.name.trim(),
|
productName: dto.name.trim(),
|
||||||
language: dto.language,
|
language: dto.language,
|
||||||
fields: form?.fields ?? [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const created = await this.products.create(
|
const created = await this.products.create(
|
||||||
@@ -96,26 +87,8 @@ export class ProductAiService {
|
|||||||
actor,
|
actor,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (form && Object.keys(draft.technicalValues).length > 0) {
|
|
||||||
const values = Object.entries(draft.technicalValues)
|
|
||||||
.filter(([, value]) => {
|
|
||||||
if (Array.isArray(value)) return value.length > 0;
|
|
||||||
return value !== '';
|
|
||||||
})
|
|
||||||
.map(([fieldKey, value]) => ({ fieldKey, value }));
|
|
||||||
|
|
||||||
if (values.length > 0) {
|
|
||||||
await this.technicalInfo.replaceForProduct(
|
|
||||||
businessIdRaw,
|
|
||||||
created.product.id,
|
|
||||||
{ values },
|
|
||||||
actor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: 'Product created with AI. Add images and variations next.',
|
message: 'Product created with AI. Add images, variations, and technical info next.',
|
||||||
product: created.product,
|
product: created.product,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -445,23 +418,11 @@ Rules:
|
|||||||
categoryName: string;
|
categoryName: string;
|
||||||
productName: string;
|
productName: string;
|
||||||
language: ProductAiLanguage;
|
language: ProductAiLanguage;
|
||||||
fields: TechnicalFormField[];
|
|
||||||
}): Promise<AiProductDraft> {
|
}): Promise<AiProductDraft> {
|
||||||
const provider = resolveAiProvider(this.config);
|
const provider = resolveAiProvider(this.config);
|
||||||
const referencePages = await loadReferencePagesFromText(input.productName);
|
const referencePages = await loadReferencePagesFromText(input.productName);
|
||||||
const usableReferences = referencePages.filter((page) => page.text);
|
const usableReferences = referencePages.filter((page) => page.text);
|
||||||
|
|
||||||
const technicalSchema = input.fields.map((field) => ({
|
|
||||||
key: field.key,
|
|
||||||
label: field.label,
|
|
||||||
type: field.type,
|
|
||||||
required: field.isRequired,
|
|
||||||
options:
|
|
||||||
field.type === 'select' || field.type === 'multi_select'
|
|
||||||
? field.options.map((option) => option.value)
|
|
||||||
: undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const inputLanguage =
|
const inputLanguage =
|
||||||
input.language === ProductAiLanguage.fa ? 'Persian (Farsi)' : 'English';
|
input.language === ProductAiLanguage.fa ? 'Persian (Farsi)' : 'English';
|
||||||
|
|
||||||
@@ -472,24 +433,18 @@ Return ONLY valid JSON with this shape:
|
|||||||
"nameFa": "Persian product name",
|
"nameFa": "Persian product name",
|
||||||
"summary": "short listing summary",
|
"summary": "short listing summary",
|
||||||
"descriptionHtml": "HTML description using <p> and <ul><li> only",
|
"descriptionHtml": "HTML description using <p> and <ul><li> only",
|
||||||
"tags": ["tag1", "tag2"],
|
"tags": ["tag1", "tag2"]
|
||||||
"technicalValues": { "fieldKey": "value" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- The user input is a research brief in ${inputLanguage}. It may include a model/product name, a product page URL, and extra notes — not a final storefront title.
|
- The user input is a research brief in ${inputLanguage}. It may include a model/product name, a product page URL, and extra notes — not a final storefront title.
|
||||||
- Infer the real product from the brief (and any URL/model clues). Do not copy URLs, markdown, or raw notes into title/nameFa.
|
- Infer the real product from the brief (and any URL/model clues). Do not copy URLs, markdown, or raw notes into title/nameFa.
|
||||||
- Write clean storefront titles: "title" in English, "nameFa" in Persian. Keep brand/model accurate.
|
- Write clean storefront titles: "title" in English, "nameFa" in Persian. Keep brand/model accurate.
|
||||||
- When referencePages are provided, treat them as the source of truth for specs, capacity, power, dimensions, and description facts.
|
- When referencePages are provided, treat them as the source of truth for capacity, power, dimensions, and description facts.
|
||||||
- Prefer the main product specs near the product title; ignore conflicting related-product / upsell blocks.
|
- Prefer the main product specs near the product title; ignore conflicting related-product / upsell blocks.
|
||||||
- Do NOT invent specs from memory when a reference page was provided.
|
- Do NOT invent specs from memory when a reference page was provided.
|
||||||
- Be factual and realistic for the identified product in the given category.
|
- Be factual and realistic for the identified product in the given category.
|
||||||
- If technical form fields are provided, fill technicalValues using exact field keys.
|
- Do NOT return technical form fields — product content only (title, names, summary, description, tags).
|
||||||
- For select fields use one allowed option value exactly.
|
|
||||||
- For multi_select fields use an array of allowed option values.
|
|
||||||
- For multi_value fields use an array of free-text strings.
|
|
||||||
- If a multi_value field is labeled «سایر ویژگیها» (or similar “other features”), put any leftover real product attributes there that do not belong in the other fields — one short string per item; do not repeat values already used elsewhere.
|
|
||||||
- For text/textarea fields use plain strings.
|
|
||||||
- Prefer the input language for summary/description tone, but always return both title and nameFa.
|
- Prefer the input language for summary/description tone, but always return both title and nameFa.
|
||||||
- descriptionHtml should be 2-4 paragraphs with key specs.
|
- descriptionHtml should be 2-4 paragraphs with key specs.
|
||||||
- tags: 3-8 relevant lowercase tags.`;
|
- tags: 3-8 relevant lowercase tags.`;
|
||||||
@@ -498,7 +453,6 @@ Rules:
|
|||||||
category: input.categoryName,
|
category: input.categoryName,
|
||||||
researchBrief: input.productName,
|
researchBrief: input.productName,
|
||||||
inputLanguage: input.language,
|
inputLanguage: input.language,
|
||||||
technicalFields: technicalSchema,
|
|
||||||
referencePages: usableReferences.map((page) => ({
|
referencePages: usableReferences.map((page) => ({
|
||||||
url: page.url,
|
url: page.url,
|
||||||
text: page.text,
|
text: page.text,
|
||||||
@@ -531,7 +485,6 @@ Rules:
|
|||||||
input: {
|
input: {
|
||||||
productName: string;
|
productName: string;
|
||||||
language: ProductAiLanguage;
|
language: ProductAiLanguage;
|
||||||
fields: TechnicalFormField[];
|
|
||||||
},
|
},
|
||||||
): AiProductDraft {
|
): AiProductDraft {
|
||||||
const fallbackName = this.shortProductLabel(input.productName);
|
const fallbackName = this.shortProductLabel(input.productName);
|
||||||
@@ -554,11 +507,6 @@ Rules:
|
|||||||
throw new BadRequestException('AI draft is missing a Persian product name');
|
throw new BadRequestException('AI draft is missing a Persian product name');
|
||||||
}
|
}
|
||||||
|
|
||||||
const technicalValues = this.normalizeTechnicalValues(
|
|
||||||
(draft.technicalValues ?? {}) as Record<string, unknown>,
|
|
||||||
input.fields,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
nameFa,
|
nameFa,
|
||||||
@@ -567,7 +515,6 @@ Rules:
|
|||||||
descriptionHtml ||
|
descriptionHtml ||
|
||||||
`<p>${summary || `${title} product description.`}</p>`,
|
`<p>${summary || `${title} product description.`}</p>`,
|
||||||
tags,
|
tags,
|
||||||
technicalValues,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user