100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
import { BadRequestException, Injectable } from '@nestjs/common';
|
||
import { ConfigService } from '@nestjs/config';
|
||
import { AuthUser } from '../auth/auth.types';
|
||
import { normalizeAiDescriptionHtml } from '../common/ai-html.util';
|
||
import {
|
||
requestAiJsonCompletion,
|
||
resolveAiProvider,
|
||
} from '../common/ai-provider.util';
|
||
import {
|
||
BusinessProfileAiFillField,
|
||
FillBusinessProfileFieldByAiDto,
|
||
} from './dto/business-profile-ai.dto';
|
||
|
||
@Injectable()
|
||
export class BusinessProfileAiService {
|
||
constructor(private readonly config: ConfigService) {}
|
||
|
||
async fillField(
|
||
_businessIdRaw: string,
|
||
dto: FillBusinessProfileFieldByAiDto,
|
||
_actor: AuthUser,
|
||
) {
|
||
const prompt = dto.prompt?.trim();
|
||
if (!prompt || prompt.length < 10) {
|
||
throw new BadRequestException('Prompt is required');
|
||
}
|
||
|
||
const nameEn = dto.nameEn?.trim() || '';
|
||
const nameFa = dto.nameFa?.trim() || '';
|
||
if (!nameEn && !nameFa) {
|
||
throw new BadRequestException(
|
||
'Enter a business name before using AI fill',
|
||
);
|
||
}
|
||
|
||
const provider = resolveAiProvider(this.config);
|
||
const isAbout = dto.field === BusinessProfileAiFillField.about;
|
||
|
||
const systemPrompt = isAbout
|
||
? `You write short business abstracts for an Iranian B2B marketplace listing.
|
||
Return ONLY valid JSON: { "text": "..." }
|
||
Follow the user's instructions for language and tone.
|
||
Keep the abstract to 1–4 sentences. No HTML. No markdown.
|
||
Prefer a clear, professional, informative style about what the business does.
|
||
Never write sales CTAs, discounts, or vague praise.`
|
||
: `You write business profile descriptions as HTML for an advanced rich-text editor on an Iranian marketplace.
|
||
Return ONLY valid JSON: { "html": "..." }
|
||
The "html" value must be a raw HTML fragment (real tags, not escaped entities, not markdown, not code fences).
|
||
Allowed tags only: p, ul, ol, li, strong, em, br.
|
||
Do not wrap in html/body/div. Do not include scripts, styles, classes, or attributes.
|
||
Follow the user's instructions for language and tone.
|
||
Prefer a professional, informative structure about the company, offerings, and strengths.
|
||
Never write sales CTAs, discounts, or vague marketing slogans.
|
||
Aim for 2–5 short paragraphs and optionally a bullet list.`;
|
||
|
||
const userPrompt = JSON.stringify({
|
||
instructions: prompt,
|
||
business: {
|
||
nameEn: nameEn || null,
|
||
nameFa: nameFa || null,
|
||
categories: dto.categories?.trim() || null,
|
||
},
|
||
});
|
||
|
||
let content: string;
|
||
try {
|
||
content = await requestAiJsonCompletion(
|
||
provider,
|
||
systemPrompt,
|
||
userPrompt,
|
||
0.5,
|
||
);
|
||
} catch (err) {
|
||
const message = err instanceof Error ? err.message : 'AI request failed';
|
||
throw new BadRequestException(message);
|
||
}
|
||
|
||
let parsed: { text?: unknown; html?: unknown };
|
||
try {
|
||
parsed = JSON.parse(content) as { text?: unknown; html?: unknown };
|
||
} catch {
|
||
throw new BadRequestException('AI returned invalid JSON');
|
||
}
|
||
|
||
if (isAbout) {
|
||
const text = String(parsed.text ?? '').trim();
|
||
if (!text) {
|
||
throw new BadRequestException('AI returned an empty abstract');
|
||
}
|
||
return { field: BusinessProfileAiFillField.about, text };
|
||
}
|
||
|
||
const html = normalizeAiDescriptionHtml(String(parsed.html ?? ''));
|
||
if (!html) {
|
||
throw new BadRequestException('AI returned an empty description');
|
||
}
|
||
return { field: BusinessProfileAiFillField.vision, html };
|
||
}
|
||
}
|