chore: add new feature for the ai generate template
This commit is contained in:
@@ -574,6 +574,18 @@ export const enum AiMessage {
|
||||
NO_RESPONSE_FROM_AI_MODEL = "خطا در دریافت پاسخ از مدل AI",
|
||||
MESSAGE_NOT_FOUND_OR_ACCESS_DENIED = "ایمیل یافت نشد یا دسترسی آن را ندارید",
|
||||
EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED = "محتوای ایمیل خالی است یا قابل استخراج نیست",
|
||||
DESCRIPTION_IS_REQUIRED = "شرح و هدف قالب الزامی است",
|
||||
DESCRIPTION_MUST_BE_A_STRING = "شرح قالب باید یک رشته باشد",
|
||||
DESCRIPTION_MUST_BE_AT_LEAST_10_CHARACTERS = "شرح قالب باید حداقل ۱۰ کاراکتر باشد",
|
||||
DESCRIPTION_MUST_BE_LESS_THAN_500_CHARACTERS = "شرح قالب نمیتواند بیشتر از ۵۰۰ کاراکتر باشد",
|
||||
THEME_MUST_BE_A_VALID_VALUE = "تم قالب باید یکی از مقادیر مجاز باشد",
|
||||
STYLE_MUST_BE_A_VALID_VALUE = "سبک قالب باید یکی از مقادیر مجاز باشد",
|
||||
PURPOSE_MUST_BE_A_VALID_VALUE = "هدف قالب باید یکی از مقادیر مجاز باشد",
|
||||
TEMPLATE_NAME_MUST_BE_A_STRING = "نام قالب باید یک رشته باشد",
|
||||
TEMPLATE_NAME_MUST_BE_LESS_THAN_100_CHARACTERS = "نام قالب نمیتواند بیشتر از ۱۰۰ کاراکتر باشد",
|
||||
PREFERRED_COLORS_MUST_BE_A_STRING = "رنگهای ترجیحی باید یک رشته باشد",
|
||||
PREFERRED_COLORS_MUST_BE_LESS_THAN_200_CHARACTERS = "رنگهای ترجیحی نمیتواند بیشتر از ۲۰۰ کاراکتر باشد",
|
||||
PREFERRED_COLORS_IS_REQUIRED = "رنگهای ترجیحی الزامی است",
|
||||
}
|
||||
|
||||
export const enum TwoFactorMessage {
|
||||
|
||||
+163
-50
@@ -2,51 +2,7 @@ import { FactoryProvider } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
import { AI_CONFIG } from "../common/constants";
|
||||
|
||||
export interface AIConfig {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
defaultModel: string;
|
||||
temperature: number;
|
||||
maxTokens: number;
|
||||
timeout: number;
|
||||
farsiPrompts: FarsiPrompts;
|
||||
}
|
||||
|
||||
export interface FarsiPrompts {
|
||||
summarization: {
|
||||
systemPrompt: string;
|
||||
promptTemplate: {
|
||||
intro: string;
|
||||
subjectLabel: string;
|
||||
fromLabel: string;
|
||||
toLabel: string;
|
||||
dateLabel: string;
|
||||
contentLabel: string;
|
||||
attachmentsLabel: string;
|
||||
analysisRequirements: string;
|
||||
summaryLengthLabel: string;
|
||||
toneLabel: string;
|
||||
};
|
||||
};
|
||||
writingAssistance: {
|
||||
systemPrompt: string;
|
||||
promptTemplate: {
|
||||
intro: string;
|
||||
mainMessageLabel: string;
|
||||
emailDetailsLabel: string;
|
||||
purposeLabel: string;
|
||||
toneLabel: string;
|
||||
lengthLabel: string;
|
||||
keyPointsLabel: string;
|
||||
contextLabel: string;
|
||||
originalSubjectLabel: string;
|
||||
originalSenderLabel: string;
|
||||
originalContentLabel: string;
|
||||
additionalInstructionsLabel: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
import { AIConfig, FarsiPrompts } from "../modules/ai-assistant/interfaces/ai.interface";
|
||||
|
||||
const FARSI_PROMPTS: FarsiPrompts = {
|
||||
summarization: {
|
||||
@@ -118,17 +74,174 @@ const FARSI_PROMPTS: FarsiPrompts = {
|
||||
additionalInstructionsLabel: "دستورالعملهای اضافی:",
|
||||
},
|
||||
},
|
||||
templateGeneration: {
|
||||
systemPrompt: `شما یک طراح ماهر قالبهای ایمیل حرفهای هستید که قالبهایی در سطح شرکتهای بزرگ مانند Figma، Stripe، Airbnb، و Apple طراحی میکنید. وظیفه شما تولید قالبهای قدرتمند، جذاب و کاملاً حرفهای است.
|
||||
|
||||
**الزامات ساختاری - بسیار مهم:**
|
||||
- قالب باید دقیقاً از ساختار PersonalityDataType پیروی کند
|
||||
- ساختار شامل سه بخش است: header, content, footer
|
||||
- هر بخش دارای columnsCount (عدد) و items (آرایه) است
|
||||
- هر item دارای id منحصر به فرد (رشته) است
|
||||
- استفاده از enums مشخص شده الزامی است
|
||||
|
||||
**Enums مجاز:**
|
||||
- HorizontalAlignment: "left" | "center" | "right"
|
||||
- VerticalAlignment: "top" | "middle" | "bottom"
|
||||
- ButtonSize: "small" | "medium" | "large"
|
||||
- ImageSize: "cover" | "contain" | "auto" | "repeat" | "repeat-x" | "repeat-y" | "no-repeat"
|
||||
- BackgroundSize: "cover" | "contain" | "round"
|
||||
- BackgroundPosition: "center center" | "top left" | "top center" | "top right" | "center left" | "center right" | "bottom left" | "bottom center" | "bottom right"
|
||||
- SocialNetworkType: "facebook" | "twitter" | "instagram" | "linkedin" | "youtube" | "telegram" | "whatsapp" | "tiktok" | "pinterest" | "snapchat" | "github" | "discord"
|
||||
- SocialIconSize: "small" | "medium" | "large"
|
||||
- SocialIconStyle: "circle" | "square" | "rounded"
|
||||
|
||||
**ساختار دقیق هر SectionItemType:**
|
||||
{
|
||||
"id": "unique-string-id",
|
||||
"backgroundColor": "#hex-color",
|
||||
"backgroundImage": "optional-url",
|
||||
"texts": [
|
||||
{
|
||||
"id": "text-unique-id",
|
||||
"text": "محتوای متن",
|
||||
"fontSize": number,
|
||||
"fontFamily": "font-name",
|
||||
"color": "#hex-color",
|
||||
"alignment": "left|center|right",
|
||||
"verticalAlignment": "top|middle|bottom"
|
||||
}
|
||||
],
|
||||
"buttons": [
|
||||
{
|
||||
"id": "button-unique-id",
|
||||
"text": "متن دکمه",
|
||||
"link": "url",
|
||||
"size": "small|medium|large",
|
||||
"isBorder": boolean,
|
||||
"borderSize": number,
|
||||
"textColor": "#hex-color",
|
||||
"backgroundColor": "#hex-color",
|
||||
"borderColor": "#hex-color",
|
||||
"alignment": "left|center|right",
|
||||
"verticalAlignment": "top|middle|bottom"
|
||||
}
|
||||
],
|
||||
"images": [
|
||||
{
|
||||
"id": "image-unique-id",
|
||||
"src": "https://real-image-url-from-internet.com",
|
||||
"alt": "توضیح تصویر",
|
||||
"size": "cover|contain|auto|repeat|repeat-x|repeat-y|no-repeat",
|
||||
"width": "100%",
|
||||
"height": "auto",
|
||||
"alignment": "left|center|right",
|
||||
"verticalAlignment": "top|middle|bottom"
|
||||
}
|
||||
],
|
||||
"socials": [
|
||||
{
|
||||
"id": "social-unique-id",
|
||||
"networkType": "facebook|twitter|instagram|linkedin|youtube|telegram|whatsapp|tiktok|pinterest|snapchat|github|discord",
|
||||
"link": "https://social-link.com",
|
||||
"size": "small|medium|large",
|
||||
"style": "circle|square|rounded",
|
||||
"color": "#hex-color",
|
||||
"alignment": "left|center|right",
|
||||
"verticalAlignment": "top|middle|bottom"
|
||||
}
|
||||
],
|
||||
"alignment": "left|center|right",
|
||||
"verticalAlignment": "top|middle|bottom",
|
||||
"style": {
|
||||
"borderRadius": number,
|
||||
"padding": "css-padding-value",
|
||||
"fontFamily": "font-family-name",
|
||||
"backgroundSize": "cover|contain|round",
|
||||
"backgroundRepeat": "no-repeat|repeat|repeat-x|repeat-y",
|
||||
"backgroundPosition": "center center|top left|..."
|
||||
}
|
||||
}
|
||||
|
||||
**الزامات طراحی حرفهای (مانند شرکتهای بزرگ):**
|
||||
1. **Header Excellence**:
|
||||
- لوگو با اندازه مناسب (حداکثر ارتفاع 60px، عرض متناسب)
|
||||
- لوگو نباید خیلی بزرگ یا غیرحرفهای باشد
|
||||
- ناوبری، پیامهای مهم، شخصیسازی
|
||||
2. **Content Power**:
|
||||
- محتوای جذاب، CTA قوی
|
||||
- تصاویر واقعی از اینترنت (نه placeholder)
|
||||
- layout تمیز و حرفهای
|
||||
3. **Footer Excellence - بسیار مهم**:
|
||||
- طراحی فوقالعاده زیبا، صاف و نرم (smooth)
|
||||
- آیکونهای شبکههای اجتماعی زیبا و هماهنگ (حداقل 4-6 شبکه)
|
||||
- اطلاعات تماس شرکت با فونت مناسب
|
||||
- لینکهای مهم (Privacy Policy, Terms, Unsubscribe) با فاصلهگذاری مناسب
|
||||
- آدرس شرکت با طراحی زیبا
|
||||
- کپیرایت و برندینگ حرفهای
|
||||
- padding و margin های مناسب برای ظاهر نرم و صاف
|
||||
- رنگبندی هارمونیک و ملایم
|
||||
4. **Professional Design**:
|
||||
- رنگهای هماهنگ و مدرن
|
||||
- typography حرفهای
|
||||
- spacing و padding مناسب
|
||||
- responsive design
|
||||
- accessibility standards
|
||||
|
||||
**استراتژی طراحی:**
|
||||
- از الگوهای شرکتهای معتبر الهام بگیرید
|
||||
- **حتماً تصاویر واقعی از اینترنت استفاده کنید** (مثل https://images.unsplash.com، https://via.placeholder.com، یا سایتهای معتبر تصویر)
|
||||
- محتوای placeholder واقعگرایانه استفاده کنید
|
||||
- CTA های قوی و جذاب طراحی کنید
|
||||
- Social proof و trust signals اضافه کنید
|
||||
- Mobile-first approach
|
||||
- Brand consistency
|
||||
- **لوگو را در اندازه مناسب و حرفهای نگه دارید**
|
||||
- **Footer را بسیار زیبا، نرم و حرفهای طراحی کنید**
|
||||
|
||||
**فرمت پاسخ JSON:**
|
||||
{
|
||||
"templateName": "نام قالب حرفهای",
|
||||
"structure": {
|
||||
"header": {
|
||||
"columnsCount": number,
|
||||
"items": [SectionItemType...]
|
||||
},
|
||||
"content": {
|
||||
"columnsCount": number,
|
||||
"items": [SectionItemType...]
|
||||
},
|
||||
"footer": {
|
||||
"columnsCount": number,
|
||||
"items": [SectionItemType...] // حتماً شامل socials و اطلاعات شرکت
|
||||
}
|
||||
},
|
||||
"description": "توضیح کامل قالب و ویژگیهای حرفهای آن",
|
||||
}`,
|
||||
promptTemplate: {
|
||||
intro: "لطفاً یک قالب ایمیل حرفهای در سطح شرکتهای بزرگ با مشخصات زیر و ساختار دقیق PersonalityDataType طراحی کنید:",
|
||||
descriptionLabel: "شرح و هدف قالب:",
|
||||
themeLabel: "تم قالب:",
|
||||
styleLabel: "سبک قالب:",
|
||||
purposeLabel: "هدف استفاده:",
|
||||
colorsLabel: "رنگهای ترجیحی:",
|
||||
templateNameLabel: "نام پیشنهادی:",
|
||||
requirementsLabel: "الزامات حرفهای:",
|
||||
structureRequirements: "- ساختار دقیق PersonalityDataType با header/content/footer کامل",
|
||||
htmlRequirements: "- HTML حرفهای، responsive و سازگار با email clients",
|
||||
designRequirements: "- استفاده از socials در footer، طراحی مانند شرکتهای بزرگ، CTA قوی",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const aiConfigProvider: FactoryProvider<AIConfig> = {
|
||||
provide: AI_CONFIG,
|
||||
useFactory: (configService: ConfigService): AIConfig => ({
|
||||
baseUrl: configService.get<string>("AI_BASE_URL", "https://api.openai.com/v1"),
|
||||
apiKey: configService.get<string>("AI_API_KEY", ""),
|
||||
defaultModel: configService.get<string>("AI_DEFAULT_MODEL", "gpt-3.5-turbo"),
|
||||
baseUrl: configService.getOrThrow<string>("AI_BASE_URL"),
|
||||
apiKey: configService.getOrThrow<string>("AI_API_KEY"),
|
||||
defaultModel: configService.getOrThrow<string>("AI_DEFAULT_MODEL"),
|
||||
temperature: configService.get<number>("AI_TEMPERATURE", 0.7),
|
||||
maxTokens: configService.get<number>("AI_MAX_TOKENS", 2000),
|
||||
timeout: configService.get<number>("AI_TIMEOUT", 30000),
|
||||
maxTokens: configService.get<number>("AI_MAX_TOKENS", 5000),
|
||||
timeout: configService.get<number>("AI_TIMEOUT", 80000),
|
||||
farsiPrompts: FARSI_PROMPTS,
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsEnum, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from "class-validator";
|
||||
|
||||
import { AiMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export enum TemplateThemeEnum {
|
||||
MODERN = "modern",
|
||||
CLASSIC = "classic",
|
||||
MINIMAL = "minimal",
|
||||
CORPORATE = "corporate",
|
||||
CREATIVE = "creative",
|
||||
NEWSLETTER = "newsletter",
|
||||
}
|
||||
|
||||
export enum TemplateStyleEnum {
|
||||
PROFESSIONAL = "professional",
|
||||
CASUAL = "casual",
|
||||
ELEGANT = "elegant",
|
||||
BOLD = "bold",
|
||||
CLEAN = "clean",
|
||||
}
|
||||
|
||||
export enum TemplatePurposeEnum {
|
||||
MARKETING = "marketing",
|
||||
NEWSLETTER = "newsletter",
|
||||
ANNOUNCEMENT = "announcement",
|
||||
WELCOME = "welcome",
|
||||
TRANSACTIONAL = "transactional",
|
||||
PROMOTIONAL = "promotional",
|
||||
INVITATION = "invitation",
|
||||
GENERAL = "general",
|
||||
}
|
||||
|
||||
export class GenerateTemplateDto {
|
||||
@IsNotEmpty({ message: AiMessage.DESCRIPTION_IS_REQUIRED })
|
||||
@IsString({ message: AiMessage.DESCRIPTION_MUST_BE_A_STRING })
|
||||
@MinLength(10, { message: AiMessage.DESCRIPTION_MUST_BE_AT_LEAST_10_CHARACTERS })
|
||||
@MaxLength(1000, { message: AiMessage.DESCRIPTION_MUST_BE_LESS_THAN_500_CHARACTERS })
|
||||
@ApiProperty({
|
||||
description: "Description of the template purpose and content requirements",
|
||||
example: "Create a professional newsletter template for announcing new products and company updates",
|
||||
minLength: 10,
|
||||
maxLength: 500,
|
||||
})
|
||||
description: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(TemplateThemeEnum)
|
||||
@ApiPropertyOptional({ enum: TemplateThemeEnum, description: "Visual theme of the template", example: TemplateThemeEnum.MODERN })
|
||||
theme?: TemplateThemeEnum;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(TemplateStyleEnum)
|
||||
@ApiPropertyOptional({ enum: TemplateStyleEnum, description: "Style approach for the template", example: TemplateStyleEnum.PROFESSIONAL })
|
||||
style?: TemplateStyleEnum;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(TemplatePurposeEnum)
|
||||
@ApiPropertyOptional({ enum: TemplatePurposeEnum, description: "Primary purpose of the template", example: TemplatePurposeEnum.NEWSLETTER })
|
||||
purpose?: TemplatePurposeEnum;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: AiMessage.TEMPLATE_NAME_MUST_BE_A_STRING })
|
||||
@MaxLength(100, { message: AiMessage.TEMPLATE_NAME_MUST_BE_LESS_THAN_100_CHARACTERS })
|
||||
@ApiPropertyOptional({ description: "Suggested name for the template", example: "Monthly Product Newsletter", maxLength: 100 })
|
||||
templateName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: AiMessage.PREFERRED_COLORS_IS_REQUIRED })
|
||||
@IsString({ message: AiMessage.PREFERRED_COLORS_MUST_BE_A_STRING })
|
||||
@MaxLength(200, { message: AiMessage.PREFERRED_COLORS_MUST_BE_LESS_THAN_200_CHARACTERS })
|
||||
@ApiPropertyOptional({
|
||||
description: "Preferred color scheme or specific colors to use",
|
||||
example: "Blue and white corporate colors, #1e3a8a, #ffffff",
|
||||
maxLength: 200,
|
||||
})
|
||||
preferredColors?: string;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Body, Controller, Post } from "@nestjs/common";
|
||||
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
|
||||
|
||||
import { GenerateTemplateDto } from "./DTO/generate-template.dto";
|
||||
import { SummarizeEmailDto } from "./DTO/summarize-email.dto";
|
||||
import { WriteEmailAssistanceDto } from "./DTO/write-email-assistance.dto";
|
||||
import { AiAssistantService } from "./services/ai-assistant.service";
|
||||
@@ -30,4 +31,10 @@ export class AiAssistantController {
|
||||
async assistEmailWriting(@Body() writeDto: WriteEmailAssistanceDto, @UserDec("wildduckUserId") wildduckUserId: string) {
|
||||
return this.aiAssistantService.assistEmailWriting(writeDto, wildduckUserId);
|
||||
}
|
||||
|
||||
@Post("generate-template")
|
||||
@ApiOperation({ summary: "Generate an email template using AI" })
|
||||
async generateTemplate(@Body() generateTemplateDto: GenerateTemplateDto) {
|
||||
return this.aiAssistantService.generateTemplate(generateTemplateDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { PriorityEnum } from "../../email/enums/email-header.enum";
|
||||
import { MessageAttachment } from "../../mail-server/interfaces/messages-response.interface";
|
||||
import { PersonalityDataType } from "../../templates/interfaces/structure.interface";
|
||||
import { TemplatePurposeEnum, TemplateStyleEnum, TemplateThemeEnum } from "../DTO/generate-template.dto";
|
||||
import { LanguageEnum, SentimentEnum, SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
|
||||
|
||||
export interface IEmailData {
|
||||
@@ -18,6 +20,36 @@ export interface SummarizationOptions {
|
||||
language?: LanguageEnum;
|
||||
}
|
||||
|
||||
export interface TemplateGenerationOptions {
|
||||
theme?: TemplateThemeEnum;
|
||||
style?: TemplateStyleEnum;
|
||||
purpose?: TemplatePurposeEnum;
|
||||
templateName?: string;
|
||||
preferredColors?: string;
|
||||
language?: LanguageEnum;
|
||||
}
|
||||
|
||||
export interface ITemplateGenerationRequest {
|
||||
description: string;
|
||||
options: TemplateGenerationOptions;
|
||||
}
|
||||
|
||||
export interface AITemplateGenerationResponse {
|
||||
templateName: string;
|
||||
structure: PersonalityDataType;
|
||||
rawHtml: string;
|
||||
description: string;
|
||||
designPrinciples: string[];
|
||||
suggestedImprovements: string[];
|
||||
colorScheme: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
accent?: string;
|
||||
background: string;
|
||||
text: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AISummaryResponse {
|
||||
summary: string;
|
||||
keyPoints: string[];
|
||||
@@ -37,4 +69,5 @@ export interface AIWritingResponse {
|
||||
export interface AIServiceInterface {
|
||||
summarizeEmail(emailData: IEmailData, options: SummarizationOptions): Promise<AISummaryResponse>;
|
||||
assistEmailWriting(prompt: string): Promise<AIWritingResponse>;
|
||||
generateTemplate(request: ITemplateGenerationRequest): Promise<AITemplateGenerationResponse>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
export interface AIConfig {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
defaultModel: string;
|
||||
temperature: number;
|
||||
maxTokens: number;
|
||||
timeout: number;
|
||||
farsiPrompts: FarsiPrompts;
|
||||
}
|
||||
|
||||
export interface FarsiPrompts {
|
||||
summarization: {
|
||||
systemPrompt: string;
|
||||
promptTemplate: {
|
||||
intro: string;
|
||||
subjectLabel: string;
|
||||
fromLabel: string;
|
||||
toLabel: string;
|
||||
dateLabel: string;
|
||||
contentLabel: string;
|
||||
attachmentsLabel: string;
|
||||
analysisRequirements: string;
|
||||
summaryLengthLabel: string;
|
||||
toneLabel: string;
|
||||
};
|
||||
};
|
||||
writingAssistance: {
|
||||
systemPrompt: string;
|
||||
promptTemplate: {
|
||||
intro: string;
|
||||
mainMessageLabel: string;
|
||||
emailDetailsLabel: string;
|
||||
purposeLabel: string;
|
||||
toneLabel: string;
|
||||
lengthLabel: string;
|
||||
keyPointsLabel: string;
|
||||
contextLabel: string;
|
||||
originalSubjectLabel: string;
|
||||
originalSenderLabel: string;
|
||||
originalContentLabel: string;
|
||||
additionalInstructionsLabel: string;
|
||||
};
|
||||
};
|
||||
templateGeneration: {
|
||||
systemPrompt: string;
|
||||
promptTemplate: {
|
||||
intro: string;
|
||||
descriptionLabel: string;
|
||||
themeLabel: string;
|
||||
styleLabel: string;
|
||||
purposeLabel: string;
|
||||
colorsLabel: string;
|
||||
templateNameLabel: string;
|
||||
requirementsLabel: string;
|
||||
structureRequirements: string;
|
||||
htmlRequirements: string;
|
||||
designRequirements: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -5,10 +5,11 @@ import { AiService } from "./ai.service";
|
||||
import { AiMessage } from "../../../common/enums/message.enum";
|
||||
import { MessageDetails } from "../../mail-server/interfaces/messages-response.interface";
|
||||
import { WildDuckMessagesService } from "../../mail-server/services/wildduck-messages.service";
|
||||
import { GenerateTemplateDto, TemplatePurposeEnum, TemplateStyleEnum, TemplateThemeEnum } from "../DTO/generate-template.dto";
|
||||
import { SummarizeEmailDto } from "../DTO/summarize-email.dto";
|
||||
import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto";
|
||||
import { SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
|
||||
import { IEmailData, SummarizationOptions } from "../interfaces/ai-service.interface";
|
||||
import { IEmailData, ITemplateGenerationRequest, SummarizationOptions, TemplateGenerationOptions } from "../interfaces/ai-service.interface";
|
||||
|
||||
@Injectable()
|
||||
export class AiAssistantService {
|
||||
@@ -37,6 +38,51 @@ export class AiAssistantService {
|
||||
return { assistance };
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
async generateTemplate(generateTemplateDto: GenerateTemplateDto) {
|
||||
if (!generateTemplateDto.description?.trim()) {
|
||||
throw new BadRequestException(AiMessage.DESCRIPTION_IS_REQUIRED);
|
||||
}
|
||||
|
||||
const templateRequest: ITemplateGenerationRequest = {
|
||||
description: generateTemplateDto.description.trim(),
|
||||
options: this.buildTemplateGenerationOptions(generateTemplateDto),
|
||||
};
|
||||
|
||||
const aiResponse = await this.aiService.generateTemplate(templateRequest);
|
||||
|
||||
return {
|
||||
template: {
|
||||
name: aiResponse.templateName,
|
||||
description: aiResponse.description,
|
||||
structure: aiResponse.structure,
|
||||
rawHtml: aiResponse.rawHtml,
|
||||
colorScheme: aiResponse.colorScheme,
|
||||
},
|
||||
designInsights: {
|
||||
designPrinciples: aiResponse.designPrinciples,
|
||||
suggestedImprovements: aiResponse.suggestedImprovements,
|
||||
},
|
||||
metadata: {
|
||||
theme: generateTemplateDto.theme || TemplateThemeEnum.MODERN,
|
||||
style: generateTemplateDto.style || TemplateStyleEnum.PROFESSIONAL,
|
||||
purpose: generateTemplateDto.purpose || TemplatePurposeEnum.GENERAL,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
private buildTemplateGenerationOptions(dto: GenerateTemplateDto): TemplateGenerationOptions {
|
||||
return {
|
||||
theme: dto.theme,
|
||||
style: dto.style,
|
||||
purpose: dto.purpose,
|
||||
templateName: dto.templateName,
|
||||
preferredColors: dto.preferredColors,
|
||||
};
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
private extractEmailData(message: MessageDetails) {
|
||||
return {
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import { Inject, Injectable, InternalServerErrorException } from "@nestjs/common";
|
||||
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
import OpenAI from "openai";
|
||||
import parseJson from "parse-json";
|
||||
|
||||
import { AI_CONFIG } from "../../../common/constants";
|
||||
import { AiMessage } from "../../../common/enums/message.enum";
|
||||
import { AIConfig } from "../../../configs/ai.config";
|
||||
import { EmailPurposeEnum, LanguageEnum, SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum";
|
||||
import { AIServiceInterface, AISummaryResponse, AIWritingResponse, IEmailData, SummarizationOptions } from "../interfaces/ai-service.interface";
|
||||
import {
|
||||
AIServiceInterface,
|
||||
AISummaryResponse,
|
||||
AITemplateGenerationResponse,
|
||||
AIWritingResponse,
|
||||
IEmailData,
|
||||
ITemplateGenerationRequest,
|
||||
SummarizationOptions,
|
||||
} from "../interfaces/ai-service.interface";
|
||||
import { AIConfig } from "../interfaces/ai.interface";
|
||||
|
||||
@Injectable()
|
||||
export class AiService implements AIServiceInterface {
|
||||
private readonly logger = new Logger(AiService.name);
|
||||
|
||||
private readonly openai: OpenAI;
|
||||
private readonly language: LanguageEnum = LanguageEnum.PERSIAN;
|
||||
private readonly defaultTone: SummaryToneEnum = SummaryToneEnum.PROFESSIONAL;
|
||||
@@ -47,7 +58,7 @@ export class AiService implements AIServiceInterface {
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
if (!response) throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL);
|
||||
|
||||
const parsedResponse = JSON.parse(response) as AISummaryResponse;
|
||||
const parsedResponse = this.parseJsonResponse<AISummaryResponse>(response);
|
||||
|
||||
return parsedResponse;
|
||||
}
|
||||
@@ -74,10 +85,51 @@ export class AiService implements AIServiceInterface {
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
if (!response) throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL);
|
||||
|
||||
const parsedResponse = JSON.parse(response) as AIWritingResponse;
|
||||
const parsedResponse = this.parseJsonResponse<AIWritingResponse>(response);
|
||||
|
||||
return parsedResponse;
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
async generateTemplate(request: ITemplateGenerationRequest): Promise<AITemplateGenerationResponse> {
|
||||
const prompt = this.buildTemplateGenerationPrompt(request);
|
||||
|
||||
const completion = await this.openai.chat.completions.create({
|
||||
model: this.aiConfig.defaultModel,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: this.getTemplateGenerationSystemPrompt(),
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
temperature: this.aiConfig.temperature,
|
||||
max_tokens: this.aiConfig.maxTokens,
|
||||
response_format: { type: "json_object" },
|
||||
});
|
||||
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
if (!response) throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL);
|
||||
|
||||
const parsedResponse = this.parseJsonResponse<AITemplateGenerationResponse>(response);
|
||||
|
||||
return parsedResponse;
|
||||
}
|
||||
//************************************************* */
|
||||
|
||||
private parseJsonResponse<T>(response: string): T {
|
||||
try {
|
||||
return parseJson(response) as T;
|
||||
} catch (error) {
|
||||
this.logger.error("error parsing json response", `${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
return response as T;
|
||||
// throw new InternalServerErrorException(AiMessage.NO_RESPONSE_FROM_AI_MODEL);
|
||||
}
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
private getSummarizationSystemPrompt(options: SummarizationOptions): string {
|
||||
const language = options.language || this.language;
|
||||
@@ -98,6 +150,11 @@ export class AiService implements AIServiceInterface {
|
||||
.replace("{language}", language);
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
private getTemplateGenerationSystemPrompt(): string {
|
||||
return this.aiConfig.farsiPrompts.templateGeneration.systemPrompt;
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
private buildSummarizationPrompt(emailData: IEmailData, options: SummarizationOptions): string {
|
||||
const template = this.aiConfig.farsiPrompts.summarization.promptTemplate;
|
||||
@@ -119,4 +176,39 @@ export class AiService implements AIServiceInterface {
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
//************************************************* */
|
||||
private buildTemplateGenerationPrompt(request: ITemplateGenerationRequest): string {
|
||||
const template = this.aiConfig.farsiPrompts.templateGeneration.promptTemplate;
|
||||
let prompt = `${template.intro}\n\n`;
|
||||
|
||||
prompt += `${template.descriptionLabel} ${request.description}\n\n`;
|
||||
|
||||
if (request.options.theme) {
|
||||
prompt += `${template.themeLabel} ${request.options.theme}\n`;
|
||||
}
|
||||
|
||||
if (request.options.style) {
|
||||
prompt += `${template.styleLabel} ${request.options.style}\n`;
|
||||
}
|
||||
|
||||
if (request.options.purpose) {
|
||||
prompt += `${template.purposeLabel} ${request.options.purpose}\n`;
|
||||
}
|
||||
|
||||
if (request.options.preferredColors) {
|
||||
prompt += `${template.colorsLabel} ${request.options.preferredColors}\n`;
|
||||
}
|
||||
|
||||
if (request.options.templateName) {
|
||||
prompt += `${template.templateNameLabel} ${request.options.templateName}\n`;
|
||||
}
|
||||
|
||||
prompt += `\n${template.requirementsLabel}\n`;
|
||||
prompt += `${template.structureRequirements}\n`;
|
||||
prompt += `${template.htmlRequirements}\n`;
|
||||
prompt += `${template.designRequirements}\n`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { UserRepository } from "../../users/repositories/user.repository";
|
||||
import { EMAIL_QUEUE_CONSTANTS } from "../constants/email-events.constant";
|
||||
import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto";
|
||||
import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto";
|
||||
import { EmailRecipientDto, EmailType, SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto";
|
||||
import { EmailRecipientDto, SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto";
|
||||
import { PriorityEnum } from "../enums/email-header.enum";
|
||||
import { IEmailMap } from "../interfaces/email-map.interface";
|
||||
|
||||
@@ -38,8 +38,6 @@ export class EmailService {
|
||||
|
||||
//########################################################
|
||||
async sendEmail(wildduckUserId: string, userId: string, sendEmailDto: SendEmailDto) {
|
||||
this.logger.log(`Sending ${EmailType.GENERAL} email from user ${wildduckUserId} to ${sendEmailDto.to.map((t) => t.address).join(", ")}`);
|
||||
|
||||
const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] });
|
||||
if (!user) throw new BadRequestException(EmailMessage.USER_NOT_FOUND);
|
||||
this.logger.log(`Processing email through business template for business: ${user.business.id}`);
|
||||
|
||||
@@ -32,17 +32,67 @@ export enum BackgroundSize {
|
||||
ROUND = "round",
|
||||
}
|
||||
|
||||
export enum BackgroundPosition {
|
||||
CENTER = "center center",
|
||||
TOP_LEFT = "top left",
|
||||
TOP_CENTER = "top center",
|
||||
TOP_RIGHT = "top right",
|
||||
CENTER_LEFT = "center left",
|
||||
CENTER_RIGHT = "center right",
|
||||
BOTTOM_LEFT = "bottom left",
|
||||
BOTTOM_CENTER = "bottom center",
|
||||
BOTTOM_RIGHT = "bottom right",
|
||||
}
|
||||
|
||||
export enum SectionName {
|
||||
HEADER = "header",
|
||||
CONTENT = "content",
|
||||
FOOTER = "footer",
|
||||
}
|
||||
|
||||
export type PersonalityDataType = {
|
||||
header: SectionType;
|
||||
content: SectionType;
|
||||
footer: SectionType;
|
||||
};
|
||||
// New enums for element selection
|
||||
export enum ElementType {
|
||||
TEXT = "text",
|
||||
BUTTON = "button",
|
||||
IMAGE = "image",
|
||||
SOCIAL = "social",
|
||||
}
|
||||
|
||||
// Social network types
|
||||
export enum SocialNetworkType {
|
||||
FACEBOOK = "facebook",
|
||||
TWITTER = "twitter",
|
||||
INSTAGRAM = "instagram",
|
||||
LINKEDIN = "linkedin",
|
||||
YOUTUBE = "youtube",
|
||||
TELEGRAM = "telegram",
|
||||
WHATSAPP = "whatsapp",
|
||||
TIKTOK = "tiktok",
|
||||
PINTEREST = "pinterest",
|
||||
SNAPCHAT = "snapchat",
|
||||
GITHUB = "github",
|
||||
DISCORD = "discord",
|
||||
}
|
||||
|
||||
export enum SocialIconSize {
|
||||
SMALL = "small",
|
||||
MEDIUM = "medium",
|
||||
LARGE = "large",
|
||||
}
|
||||
|
||||
export enum SocialIconStyle {
|
||||
CIRCLE = "circle",
|
||||
SQUARE = "square",
|
||||
ROUNDED = "rounded",
|
||||
}
|
||||
|
||||
// New type for selected element
|
||||
export type SelectedElement = {
|
||||
type: ElementType;
|
||||
elementId: string;
|
||||
itemId: string;
|
||||
sectionKey: keyof PersonalityDataType;
|
||||
} | null;
|
||||
|
||||
export type SectionType = {
|
||||
columnsCount: number;
|
||||
@@ -56,6 +106,7 @@ export type SectionItemType = {
|
||||
texts: TextType[];
|
||||
buttons: ButtonType[];
|
||||
images: ImageType[];
|
||||
socials: SocialType[];
|
||||
alignment?: HorizontalAlignment;
|
||||
verticalAlignment?: VerticalAlignment;
|
||||
style?: {
|
||||
@@ -103,14 +154,33 @@ export type ImageType = {
|
||||
verticalAlignment?: VerticalAlignment;
|
||||
};
|
||||
|
||||
export type SocialType = {
|
||||
id: string;
|
||||
networkType: SocialNetworkType;
|
||||
link: string;
|
||||
size: SocialIconSize;
|
||||
style: SocialIconStyle;
|
||||
color?: string;
|
||||
alignment?: HorizontalAlignment;
|
||||
verticalAlignment?: VerticalAlignment;
|
||||
};
|
||||
|
||||
export type TemplateType = {
|
||||
name: string;
|
||||
rawHtml: string;
|
||||
structure: PersonalityDataType;
|
||||
thumbnailUrl: string;
|
||||
};
|
||||
|
||||
//main type for the template structure
|
||||
export type PersonalityDataType = {
|
||||
header: SectionType;
|
||||
content: SectionType;
|
||||
footer: SectionType;
|
||||
};
|
||||
|
||||
export interface EmailContent {
|
||||
text?: string;
|
||||
html?: string;
|
||||
subject?: string;
|
||||
}
|
||||
|
||||
export interface ProcessedEmailResult {
|
||||
content: string;
|
||||
isHtml: boolean;
|
||||
hasTemplate: boolean;
|
||||
}
|
||||
|
||||
@@ -1,52 +1,36 @@
|
||||
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
|
||||
import { TemplatesService } from "./templates.service";
|
||||
import { UserMessage } from "../../../common/enums/message.enum";
|
||||
import { UserRepository } from "../../users/repositories/user.repository";
|
||||
import { EmailContent, ProcessedEmailResult } from "../interfaces/structure.interface";
|
||||
import { EmailContent } from "../interfaces/structure.interface";
|
||||
|
||||
@Injectable()
|
||||
export class TemplateProcessorService {
|
||||
private readonly logger = new Logger(TemplateProcessorService.name);
|
||||
|
||||
constructor(
|
||||
private readonly templatesService: TemplatesService,
|
||||
private readonly userRepository: UserRepository,
|
||||
) {}
|
||||
|
||||
//**************************************************** */
|
||||
async processEmailContent(businessId: string, userId: string, emailContent: EmailContent): Promise<ProcessedEmailResult> {
|
||||
try {
|
||||
// Get selected template for the business
|
||||
const selectedTemplate = await this.getSelectedTemplate(businessId, userId);
|
||||
async processEmailContent(businessId: string, userId: string, emailContent: EmailContent) {
|
||||
const selectedTemplate = await this.getSelectedTemplate(businessId, userId);
|
||||
|
||||
if (!selectedTemplate) {
|
||||
// No template - return original text content
|
||||
return {
|
||||
content: emailContent.text || emailContent.html || "",
|
||||
isHtml: false,
|
||||
hasTemplate: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Process with template
|
||||
const processedContent = this.injectContentIntoTemplate(selectedTemplate.rawHtml, emailContent);
|
||||
|
||||
return {
|
||||
content: processedContent,
|
||||
isHtml: true,
|
||||
hasTemplate: true,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to process email for business ${businessId}:`, error);
|
||||
|
||||
// Fallback to original content
|
||||
if (!selectedTemplate) {
|
||||
return {
|
||||
content: emailContent.text || emailContent.html || "",
|
||||
isHtml: false,
|
||||
hasTemplate: false,
|
||||
};
|
||||
}
|
||||
|
||||
const processedContent = await this.injectContentIntoTemplate(selectedTemplate.rawHtml, emailContent);
|
||||
|
||||
return {
|
||||
content: processedContent,
|
||||
isHtml: true,
|
||||
hasTemplate: true,
|
||||
};
|
||||
}
|
||||
|
||||
//**************************************************** */
|
||||
@@ -54,14 +38,13 @@ export class TemplateProcessorService {
|
||||
const user = await this.userRepository.findOne({ id: userId, business: { id: businessId }, deletedAt: null }, { populate: ["template"] });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
|
||||
if (user.template) return user.template;
|
||||
if (user.template && user.template.rawHtml) return user.template;
|
||||
|
||||
const templates = await this.templatesService.getTemplatesByBusinessId(businessId);
|
||||
return templates.find((template) => template.selected) || null;
|
||||
return this.templatesService.getSelectedTemplateByBusinessId(businessId);
|
||||
}
|
||||
|
||||
//**************************************************** */
|
||||
private injectContentIntoTemplate(templateHtml: string | undefined, emailContent: EmailContent): string {
|
||||
private async injectContentIntoTemplate(templateHtml: string | undefined, emailContent: EmailContent) {
|
||||
if (!templateHtml) {
|
||||
return emailContent.text || emailContent.html || "";
|
||||
}
|
||||
|
||||
@@ -102,6 +102,11 @@ export class TemplatesService {
|
||||
return this.templateRepository.find({ business: { id: businessId }, deletedAt: null }, { populate: ["business"] });
|
||||
}
|
||||
|
||||
//**************************************************** */
|
||||
async getSelectedTemplateByBusinessId(businessId: string) {
|
||||
return this.templateRepository.findOne({ business: { id: businessId }, deletedAt: null, selected: true }, { populate: ["business"] });
|
||||
}
|
||||
|
||||
//**************************************************** */
|
||||
async setSelectedTemplate(id: string, businessId: string) {
|
||||
const template = await this.templateRepository.findTemplateByIdWithoutRawHtmlAndStructure(id, businessId);
|
||||
|
||||
@@ -67,6 +67,8 @@ export class User extends BaseEntity {
|
||||
@OneToMany(() => RefreshToken, (token) => token.user)
|
||||
refreshTokens = new Collection<RefreshToken>(this);
|
||||
|
||||
//=========================
|
||||
|
||||
@ManyToOne(() => Role, { deleteRule: "restrict" })
|
||||
role!: Role;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user