chore: add new feature for the ai generate template
This commit is contained in:
@@ -1,279 +0,0 @@
|
||||
# Email Polling System
|
||||
|
||||
This document describes the email polling system that replaced the webhook-based approach for real-time email notifications.
|
||||
|
||||
## Overview
|
||||
|
||||
The email polling system uses **BullMQ** (Redis-based job queue) to periodically check for new emails when users are connected via WebSocket. This approach is more reliable than webhooks since it doesn't depend on external webhook delivery.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Components
|
||||
|
||||
1. **EmailPollingService** - Manages polling jobs for connected users
|
||||
2. **EmailPollingProcessor** - Processes email polling jobs and checks for new emails
|
||||
3. **EmailGateway** - WebSocket gateway that starts/stops polling on user connection/disconnection
|
||||
4. **BullMQ Queue** - Redis-based job queue for scheduling recurring email checks
|
||||
|
||||
### Flow
|
||||
|
||||
```
|
||||
User connects to WebSocket
|
||||
↓
|
||||
EmailGateway.handleConnection()
|
||||
↓
|
||||
EmailPollingService.startPollingForUser()
|
||||
↓
|
||||
BullMQ creates recurring job (every 30 seconds)
|
||||
↓
|
||||
EmailPollingProcessor.process() - checks for new emails
|
||||
↓
|
||||
If new emails found → notify via WebSocket
|
||||
↓
|
||||
User disconnects
|
||||
↓
|
||||
EmailGateway.handleDisconnect()
|
||||
↓
|
||||
EmailPollingService.stopPollingForUser()
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Required environment variables (see `environment.sample`):
|
||||
|
||||
```bash
|
||||
# Redis for BullMQ
|
||||
REDIS_URI=redis://localhost:6379
|
||||
|
||||
# WildDuck Mail Server
|
||||
WILDDUCK_SERVER=http://localhost:8080
|
||||
WILDDUCK_API_TOKEN=your-wildduck-access-token
|
||||
|
||||
# Email Polling (optional)
|
||||
EMAIL_POLLING_INTERVAL=30000 # 30 seconds
|
||||
EMAIL_POLLING_ENABLED=true
|
||||
```
|
||||
|
||||
### BullMQ Configuration
|
||||
|
||||
The system uses the existing BullMQ configuration from `src/configs/bullmq.config.ts`:
|
||||
|
||||
```typescript
|
||||
export function bullMqConfig(): SharedBullAsyncConfiguration {
|
||||
return {
|
||||
connection: {
|
||||
url: configService.getOrThrow<string>("REDIS_URI"),
|
||||
},
|
||||
prefix: "dmail",
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: false,
|
||||
attempts: 5,
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Email Polling Service
|
||||
|
||||
Located at `src/modules/email-utils/services/email-polling.service.ts`
|
||||
|
||||
**Key Methods:**
|
||||
|
||||
- `startPollingForUser(userId, connectionId)` - Start polling when user connects
|
||||
- `stopPollingForUser(userId)` - Stop polling when user disconnects
|
||||
- `getPollingStats()` - Get statistics about active polling jobs
|
||||
|
||||
**Job Configuration:**
|
||||
|
||||
```typescript
|
||||
const job = await this.emailPollingQueue.add(`email-poll-${userId}`, jobData, {
|
||||
repeat: {
|
||||
every: 30000, // 30 seconds interval
|
||||
},
|
||||
removeOnComplete: 5,
|
||||
removeOnFail: 10,
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 2000,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Email Polling Processor
|
||||
|
||||
Located at `src/modules/email-utils/queue/email-polling.processor.ts`
|
||||
|
||||
**Processing Logic:**
|
||||
|
||||
1. Check if user is still connected (skip if disconnected)
|
||||
2. Get user's mailbox information
|
||||
3. Query WildDuck API for recent emails since last check
|
||||
4. For each new email:
|
||||
- Send WebSocket notification via `EmailGateway.notifyNewEmail()`
|
||||
- Update unread count via `EmailGateway.notifyUnreadCountUpdate()`
|
||||
5. Update job data with new timestamp for next run
|
||||
|
||||
### WebSocket Integration
|
||||
|
||||
The `EmailGateway` automatically manages polling:
|
||||
|
||||
```typescript
|
||||
// On user connection
|
||||
await this.emailPollingService.startPollingForUser(user.id, client.id);
|
||||
|
||||
// On user disconnection
|
||||
await this.emailPollingService.stopPollingForUser(userInfo.userId);
|
||||
```
|
||||
|
||||
## WebSocket Events
|
||||
|
||||
The system emits these WebSocket events when new emails are found:
|
||||
|
||||
### `new_email` Event
|
||||
|
||||
```typescript
|
||||
{
|
||||
userId: string;
|
||||
messageId: number;
|
||||
subject: string;
|
||||
from: { name?: string; address: string };
|
||||
to: Array<{ name?: string; address: string }>;
|
||||
hasAttachments: boolean;
|
||||
timestamp: string;
|
||||
mailboxId: string;
|
||||
mailboxName: string;
|
||||
isRead: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### `unread_count_updated` Event
|
||||
|
||||
```typescript
|
||||
{
|
||||
userId: string;
|
||||
totalUnread: number;
|
||||
mailboxCounts: Record<string, number>;
|
||||
count: number;
|
||||
timestamp: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring & Debugging
|
||||
|
||||
### Check Active Polling Jobs
|
||||
|
||||
```typescript
|
||||
const stats = emailPollingService.getPollingStats();
|
||||
console.log({
|
||||
activeJobs: stats.activeJobs,
|
||||
userIds: stats.userIds,
|
||||
connections: stats.connections,
|
||||
});
|
||||
```
|
||||
|
||||
### BullMQ Dashboard
|
||||
|
||||
You can use BullMQ's web dashboard to monitor jobs:
|
||||
|
||||
```bash
|
||||
npm install -g @bull-board/express
|
||||
# Then access the dashboard at http://localhost:3000/admin/queues
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
The system logs important events:
|
||||
|
||||
```
|
||||
[EmailPollingService] Started email polling for user 12345 with job ID: email-poll-12345
|
||||
[EmailPollingProcessor] Found 3 new emails for user 12345
|
||||
[EmailPollingProcessor] Notified user 12345 of 3 new emails
|
||||
[EmailPollingService] Stopped email polling for user 12345
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Polling Interval
|
||||
|
||||
- Default: 30 seconds
|
||||
- Adjustable via `EMAIL_POLLING_INTERVAL` environment variable
|
||||
- Balance between real-time updates and API load
|
||||
|
||||
### Job Management
|
||||
|
||||
- Jobs are automatically cleaned up when users disconnect
|
||||
- Failed jobs are retried up to 3 times with exponential backoff
|
||||
- Completed jobs are kept (last 5) for debugging
|
||||
|
||||
### Resource Usage
|
||||
|
||||
- Each connected user creates one recurring BullMQ job
|
||||
- Jobs only run when users are connected (checked at start of each job)
|
||||
- Redis is used for job queue storage
|
||||
|
||||
## Advantages over Webhooks
|
||||
|
||||
1. **Reliability** - No dependency on external webhook delivery
|
||||
2. **Connection Awareness** - Only polls when users are actually connected
|
||||
3. **Error Handling** - Built-in retry mechanism with exponential backoff
|
||||
4. **Monitoring** - Full visibility into job status and performance
|
||||
5. **Scalability** - Redis-based queue can handle high loads
|
||||
6. **Control** - Can adjust polling frequency per user if needed
|
||||
|
||||
## Migration from Webhooks
|
||||
|
||||
The old webhook system has been completely removed:
|
||||
|
||||
**Deleted Files:**
|
||||
|
||||
- `src/modules/email-utils/controllers/webhook.controller.ts`
|
||||
- `src/modules/email-utils/services/webhook-management.service.ts`
|
||||
- `src/modules/email-utils/services/webhook-email-events.service.ts`
|
||||
- `src/modules/email/interfaces/webhook-events.interface.ts`
|
||||
|
||||
**Updated Files:**
|
||||
|
||||
- `src/modules/email-utils/email-utils.module.ts` - Added BullMQ and new services
|
||||
- `src/modules/email-gateway/email.gateway.ts` - Added polling integration
|
||||
- `src/modules/email-gateway/email-gateway.module.ts` - Added EmailUtilsModule import
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Issue: Jobs not running**
|
||||
|
||||
- Check Redis connection
|
||||
- Verify `REDIS_URI` environment variable
|
||||
- Check BullMQ logs
|
||||
|
||||
**Issue: Users not receiving notifications**
|
||||
|
||||
- Verify user is connected to WebSocket
|
||||
- Check if polling job is active: `emailPollingService.getPollingStats()`
|
||||
- Verify WildDuck API credentials and server URL
|
||||
|
||||
**Issue: High Redis memory usage**
|
||||
|
||||
- Adjust `removeOnComplete` and `removeOnFail` values
|
||||
- Consider shorter polling intervals for fewer concurrent jobs
|
||||
|
||||
**Issue: API rate limiting**
|
||||
|
||||
- Increase polling interval
|
||||
- Implement user-specific rate limiting
|
||||
- Use WildDuck API bulk endpoints if available
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Dynamic Polling Intervals** - Adjust frequency based on user activity
|
||||
2. **Priority Users** - More frequent polling for premium users
|
||||
3. **Email Filtering** - Only notify for emails matching user preferences
|
||||
4. **Batch Processing** - Process multiple users in single API calls
|
||||
5. **Caching** - Cache recent emails to reduce API calls
|
||||
@@ -1,77 +0,0 @@
|
||||
# Database Configuration
|
||||
DATABASE_URL=postgresql://username:password@localhost:5432/dmail_db
|
||||
|
||||
# Redis Configuration (for BullMQ)
|
||||
REDIS_URI=redis://localhost:6379
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-super-secret-jwt-key-here
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# Mail Server Configuration (WildDuck API)
|
||||
WILDDUCK_SERVER=http://localhost:8080
|
||||
WILDDUCK_API_TOKEN=your-wildduck-access-token-here
|
||||
|
||||
# Email Polling Configuration
|
||||
EMAIL_POLLING_INTERVAL=30000 # 30 seconds (in milliseconds)
|
||||
EMAIL_POLLING_ENABLED=true
|
||||
|
||||
# WebSocket Configuration
|
||||
WS_CORS_ORIGIN=http://localhost:3000
|
||||
WS_TRANSPORTS=websocket,polling
|
||||
|
||||
# Application Configuration
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
APP_NAME=dmail-api
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# AWS S3 Configuration (for file uploads)
|
||||
AWS_REGION=us-east-1
|
||||
AWS_ACCESS_KEY_ID=your-aws-access-key
|
||||
AWS_SECRET_ACCESS_KEY=your-aws-secret-key
|
||||
AWS_S3_BUCKET=your-s3-bucket-name
|
||||
|
||||
# Notification Configuration (for push notifications)
|
||||
VAPID_PUBLIC_KEY=your-vapid-public-key
|
||||
VAPID_PRIVATE_KEY=your-vapid-private-key
|
||||
VAPID_SUBJECT=mailto:admin@yourapp.com
|
||||
|
||||
# Rate Limiting
|
||||
RATE_LIMIT_TTL=60000 # 1 minute
|
||||
RATE_LIMIT_LIMIT=100 # 100 requests per minute per IP
|
||||
|
||||
# Cache Configuration
|
||||
CACHE_TTL=300000 # 5 minutes
|
||||
|
||||
# Business Configuration
|
||||
BUSINESS_QUOTA_DEFAULT=1000
|
||||
BUSINESS_QUOTA_PREMIUM=10000
|
||||
|
||||
# AI Assistant Configuration (OpenAI)
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
OPENAI_MODEL=gpt-3.5-turbo
|
||||
|
||||
# CORS Configuration
|
||||
CORS_ORIGINS=http://localhost:3000,https://yourapp.com
|
||||
|
||||
# Email Templates
|
||||
EMAIL_TEMPLATE_DIR=./templates
|
||||
EMAIL_FROM_ADDRESS=noreply@yourapp.com
|
||||
EMAIL_FROM_NAME=Your App Name
|
||||
|
||||
# Monitoring & Logging
|
||||
SENTRY_DSN=your-sentry-dsn-here
|
||||
LOG_FORMAT=combined
|
||||
LOG_FILE_PATH=./logs/app.log
|
||||
|
||||
# Development Settings
|
||||
SWAGGER_ENABLED=true
|
||||
DEBUG=false
|
||||
|
||||
# Queue Configuration
|
||||
QUEUE_REDIS_URL=redis://localhost:6379
|
||||
QUEUE_PREFIX=dmail
|
||||
QUEUE_DEFAULT_JOB_OPTIONS_REMOVE_ON_COMPLETE=true
|
||||
QUEUE_DEFAULT_JOB_OPTIONS_REMOVE_ON_FAIL=false
|
||||
QUEUE_DEFAULT_JOB_OPTIONS_ATTEMPTS=5
|
||||
@@ -67,6 +67,7 @@
|
||||
"fastify": "^5.4.0",
|
||||
"nodemailer": "^7.0.4",
|
||||
"openai": "^5.10.2",
|
||||
"parse-json": "^8.3.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
|
||||
Generated
+19
@@ -119,6 +119,9 @@ importers:
|
||||
openai:
|
||||
specifier: ^5.10.2
|
||||
version: 5.10.2
|
||||
parse-json:
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0
|
||||
passport-jwt:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
@@ -3918,6 +3921,10 @@ packages:
|
||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||
engines: {node: '>=0.8.19'}
|
||||
|
||||
index-to-position@1.1.0:
|
||||
resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
inflight@1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
||||
@@ -5114,6 +5121,10 @@ packages:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
parse-json@8.3.0:
|
||||
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||
resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
|
||||
|
||||
@@ -11028,6 +11039,8 @@ snapshots:
|
||||
|
||||
imurmurhash@0.1.4: {}
|
||||
|
||||
index-to-position@1.1.0: {}
|
||||
|
||||
inflight@1.0.6:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
@@ -12637,6 +12650,12 @@ snapshots:
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
lines-and-columns: 1.2.4
|
||||
|
||||
parse-json@8.3.0:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
index-to-position: 1.1.0
|
||||
type-fest: 4.41.0
|
||||
|
||||
parse5-htmlparser2-tree-adapter@7.1.0:
|
||||
dependencies:
|
||||
domhandler: 5.0.3
|
||||
|
||||
@@ -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