main
All checks were successful
Backend Deploy 🚀 / build-and-deploy (push) Successful in 2m1s

This commit is contained in:
Harun CAN
2026-02-10 12:27:14 +03:00
parent 80f53511d8
commit fc88faddb9
141 changed files with 35961 additions and 101 deletions

View File

@@ -0,0 +1,256 @@
// Content Controller - API endpoints for content management
// Path: src/modules/content/content.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
ParseUUIDPipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { ContentService } from './content.service';
import { MasterContentService } from './services/master-content.service';
import type { CreateMasterContentDto } from './services/master-content.service';
import { BuildingBlocksService } from './services/building-blocks.service';
import { WritingStylesService } from './services/writing-styles.service';
import type { WritingStyleConfig } from './services/writing-styles.service';
import { ContentVariationsService } from './services/content-variations.service';
import type { VariationConfig } from './services/content-variations.service';
import { PlatformAdaptersService } from './services/platform-adapters.service';
import { CurrentUser } from '../../common/decorators';
import { SocialPlatform, ContentStatus, MasterContentType } from '@prisma/client';
@ApiTags('content')
@ApiBearerAuth()
@Controller('content')
export class ContentController {
constructor(
private readonly contentService: ContentService,
private readonly masterContentService: MasterContentService,
private readonly buildingBlocksService: BuildingBlocksService,
private readonly writingStylesService: WritingStylesService,
private readonly variationsService: ContentVariationsService,
private readonly platformsService: PlatformAdaptersService,
) { }
// ==================== Master Content ====================
@Post('master')
@ApiOperation({ summary: 'Create master content' })
async createMaster(
@CurrentUser('id') userId: string,
@Body() dto: CreateMasterContentDto,
) {
return this.masterContentService.create(userId, dto);
}
@Get('master')
@ApiOperation({ summary: 'Get user master contents' })
@ApiQuery({ name: 'type', required: false, enum: MasterContentType })
async getMasterContents(
@CurrentUser('id') userId: string,
@Query('type') type?: MasterContentType,
@Query('limit') limit?: number,
@Query('offset') offset?: number,
) {
return this.masterContentService.getByUser(userId, {
type,
limit: limit ? Number(limit) : undefined,
offset: offset ? Number(offset) : undefined,
});
}
@Get('master/:id')
@ApiOperation({ summary: 'Get master content by ID' })
async getMasterById(@Param('id', ParseUUIDPipe) id: string) {
return this.masterContentService.getById(id);
}
@Post('master/:id/repurpose')
@ApiOperation({ summary: 'Repurpose master content to multiple platforms' })
async repurposeMaster(
@Param('id', ParseUUIDPipe) id: string,
@Body('platforms') platforms: string[],
) {
return this.masterContentService.repurpose(id, platforms);
}
// ==================== Platform Content ====================
@Post()
@ApiOperation({ summary: 'Create platform-specific content' })
async createContent(
@CurrentUser('id') userId: string,
@Body() dto: { masterContentId: string; platform: SocialPlatform; body?: string; scheduledAt?: string },
) {
return this.contentService.createFromMaster(userId, {
...dto,
scheduledAt: dto.scheduledAt ? new Date(dto.scheduledAt) : undefined,
});
}
@Get()
@ApiOperation({ summary: 'Get user contents' })
@ApiQuery({ name: 'platform', required: false, enum: SocialPlatform })
@ApiQuery({ name: 'status', required: false, enum: ContentStatus })
async getContents(
@CurrentUser('id') userId: string,
@Query('platform') platform?: SocialPlatform,
@Query('status') status?: ContentStatus,
@Query('limit') limit?: number,
@Query('offset') offset?: number,
) {
return this.contentService.getByUser(userId, {
platform,
status,
limit: limit ? Number(limit) : undefined,
offset: offset ? Number(offset) : undefined,
});
}
@Get('calendar')
@ApiOperation({ summary: 'Get content calendar' })
async getCalendar(
@CurrentUser('id') userId: string,
@Query('start') start: string,
@Query('end') end: string,
) {
return this.contentService.getCalendar(userId, new Date(start), new Date(end));
}
@Get('analytics')
@ApiOperation({ summary: 'Get content analytics' })
async getAnalytics(
@CurrentUser('id') userId: string,
@Query('period') period: 'week' | 'month' | 'year' = 'month',
) {
return this.contentService.getAnalytics(userId, period);
}
@Get(':id')
@ApiOperation({ summary: 'Get content by ID' })
async getById(@Param('id', ParseUUIDPipe) id: string) {
return this.contentService.getById(id);
}
@Put(':id')
@ApiOperation({ summary: 'Update content' })
async updateContent(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: { body?: string; status?: ContentStatus; scheduledAt?: string },
) {
return this.contentService.update(id, {
...dto,
scheduledAt: dto.scheduledAt ? new Date(dto.scheduledAt) : undefined,
});
}
@Post(':id/publish')
@ApiOperation({ summary: 'Publish content' })
async publishContent(
@Param('id', ParseUUIDPipe) id: string,
@Body('publishedUrl') publishedUrl?: string,
) {
return this.contentService.publish(id, publishedUrl);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete content' })
async deleteContent(@Param('id', ParseUUIDPipe) id: string) {
return this.contentService.delete(id);
}
// ==================== Variations ====================
@Post(':id/variations')
@ApiOperation({ summary: 'Generate content variations' })
async generateVariations(
@Param('id', ParseUUIDPipe) id: string,
@Body() config: VariationConfig,
) {
return this.variationsService.generateVariations(id, config);
}
@Get(':id/variations/metrics')
@ApiOperation({ summary: 'Get variation performance metrics' })
async getVariationMetrics(@Param('id', ParseUUIDPipe) id: string) {
return this.variationsService.getPerformanceMetrics(id);
}
@Post(':id/variations/winner')
@ApiOperation({ summary: 'Select winning variation' })
async selectWinner(
@Param('id', ParseUUIDPipe) id: string,
@Body('metric') metric: 'ctr' | 'engagementRate' | 'conversionRate',
) {
return this.variationsService.selectWinner(id, metric);
}
// ==================== Writing Styles ====================
@Get('styles')
@ApiOperation({ summary: 'Get all writing styles' })
async getWritingStyles(@CurrentUser('id') userId: string) {
return this.writingStylesService.getAll(userId);
}
@Post('styles')
@ApiOperation({ summary: 'Create custom writing style' })
async createWritingStyle(
@CurrentUser('id') userId: string,
@Body() config: WritingStyleConfig,
) {
return this.writingStylesService.create(userId, config);
}
@Put('styles/:id/default')
@ApiOperation({ summary: 'Set default writing style' })
async setDefaultStyle(
@CurrentUser('id') userId: string,
@Param('id') styleId: string,
) {
return this.writingStylesService.setDefault(userId, styleId);
}
// ==================== Building Blocks ====================
@Get('blocks/:type')
@ApiOperation({ summary: 'Get building blocks by type' })
async getBuildingBlocks(
@CurrentUser('id') userId: string,
@Param('type') type: string,
@Query('limit') limit?: number,
) {
return this.buildingBlocksService.getByType(userId, type, limit ? Number(limit) : undefined);
}
// ==================== Platforms ====================
@Get('platforms/config')
@ApiOperation({ summary: 'Get all platform configurations' })
async getPlatformConfigs() {
return this.platformsService.getAllConfigs();
}
@Get('platforms/:platform/config')
@ApiOperation({ summary: 'Get specific platform config' })
async getPlatformConfig(@Param('platform') platform: SocialPlatform) {
return this.platformsService.getConfig(platform);
}
@Post('platforms/adapt')
@ApiOperation({ summary: 'Adapt content for platform' })
async adaptContent(
@Body() dto: { content: string; sourcePlatform: SocialPlatform; targetPlatform: SocialPlatform },
) {
return {
adapted: this.platformsService.adapt(dto.content, dto.sourcePlatform, dto.targetPlatform),
validation: this.platformsService.validate(dto.content, dto.targetPlatform),
};
}
}

View File

@@ -0,0 +1,25 @@
// Master Content Module - Content generation pipeline
// Path: src/modules/content/content.module.ts
import { Module } from '@nestjs/common';
import { ContentService } from './content.service';
import { ContentController } from './content.controller';
import { MasterContentService } from './services/master-content.service';
import { BuildingBlocksService } from './services/building-blocks.service';
import { WritingStylesService } from './services/writing-styles.service';
import { ContentVariationsService } from './services/content-variations.service';
import { PlatformAdaptersService } from './services/platform-adapters.service';
@Module({
providers: [
ContentService,
MasterContentService,
BuildingBlocksService,
WritingStylesService,
ContentVariationsService,
PlatformAdaptersService,
],
controllers: [ContentController],
exports: [ContentService, MasterContentService],
})
export class ContentModule { }

View File

@@ -0,0 +1,225 @@
// Content Service - Main orchestrator for content operations
// Path: src/modules/content/content.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../database/prisma.service';
import { MasterContentService } from './services/master-content.service';
import { BuildingBlocksService } from './services/building-blocks.service';
import { WritingStylesService } from './services/writing-styles.service';
import { ContentVariationsService } from './services/content-variations.service';
import { PlatformAdaptersService } from './services/platform-adapters.service';
import { SocialPlatform, ContentStatus } from '@prisma/client';
export interface CreateContentDto {
masterContentId: string;
platform: SocialPlatform;
body?: string;
scheduledAt?: Date;
}
@Injectable()
export class ContentService {
private readonly logger = new Logger(ContentService.name);
constructor(
private readonly prisma: PrismaService,
private readonly masterContent: MasterContentService,
private readonly buildingBlocks: BuildingBlocksService,
private readonly writingStyles: WritingStylesService,
private readonly variations: ContentVariationsService,
private readonly platforms: PlatformAdaptersService,
) { }
/**
* Create platform-specific content from master content
*/
async createFromMaster(userId: string, dto: CreateContentDto) {
const master = await this.masterContent.getById(dto.masterContentId);
if (!master) {
throw new Error('Master content not found');
}
// Get platform config
const platformConfig = this.platforms.getConfig(dto.platform);
// Adapt content for platform
let body = dto.body || master.body || '';
body = this.platforms.format(body, dto.platform);
// Validate content
const validation = this.platforms.validate(body, dto.platform);
if (!validation.valid) {
this.logger.warn(`Content validation issues: ${validation.issues.join(', ')}`);
}
// Create content record
const content = await this.prisma.content.create({
data: {
userId,
masterContentId: dto.masterContentId,
type: dto.platform as any,
body,
status: dto.scheduledAt ? ContentStatus.SCHEDULED : ContentStatus.DRAFT,
scheduledAt: dto.scheduledAt,
},
});
return {
content,
platformConfig,
validation,
};
}
/**
* Get user's content with filters
*/
async getByUser(
userId: string,
options?: {
platform?: SocialPlatform;
status?: ContentStatus;
limit?: number;
offset?: number;
},
) {
return this.prisma.content.findMany({
where: {
userId,
...(options?.platform && { platform: options.platform }),
...(options?.status && { status: options.status }),
},
orderBy: { createdAt: 'desc' },
take: options?.limit || 20,
skip: options?.offset || 0,
include: {
masterContent: { select: { id: true, title: true, type: true } },
variants: { select: { id: true, name: true, isWinner: true } },
_count: { select: { variants: true } },
},
});
}
/**
* Get content by ID
*/
async getById(id: string) {
return this.prisma.content.findUnique({
where: { id },
include: {
masterContent: true,
variants: true,
approvals: true,
},
});
}
/**
* Update content
*/
async update(id: string, data: { body?: string; status?: ContentStatus; scheduledAt?: Date }) {
return this.prisma.content.update({
where: { id },
data,
});
}
/**
* Publish content (mark as published)
*/
async publish(id: string, publishedUrl?: string) {
return this.prisma.content.update({
where: { id },
data: {
status: ContentStatus.PUBLISHED,
publishedAt: new Date(),
publishedUrl,
},
});
}
/**
* Delete content
*/
async delete(id: string) {
return this.prisma.content.delete({
where: { id },
});
}
/**
* Get content calendar for user
*/
async getCalendar(userId: string, startDate: Date, endDate: Date) {
return this.prisma.content.findMany({
where: {
userId,
OR: [
{ scheduledAt: { gte: startDate, lte: endDate } },
{ publishedAt: { gte: startDate, lte: endDate } },
],
},
orderBy: [{ scheduledAt: 'asc' }, { publishedAt: 'asc' }],
select: {
id: true,
type: true,
status: true,
scheduledAt: true,
publishedAt: true,
masterContent: { select: { title: true } },
},
});
}
/**
* Get content analytics
*/
async getAnalytics(userId: string, period: 'week' | 'month' | 'year') {
const now = new Date();
let startDate: Date;
switch (period) {
case 'week':
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
break;
case 'month':
startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
break;
case 'year':
startDate = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000);
break;
}
const contents = await this.prisma.content.findMany({
where: {
userId,
publishedAt: { gte: startDate },
},
include: {
variants: true,
},
});
// Aggregate by platform
const byPlatform = contents.reduce(
(acc, content) => {
if (!acc[content.type]) {
acc[content.type] = { count: 0, totalEngagement: 0 };
}
acc[content.type].count++;
acc[content.type].totalEngagement += content.variants.reduce(
(sum, v) => sum + (v.engagements || 0),
0,
);
return acc;
},
{} as Record<string, { count: number; totalEngagement: number }>,
);
return {
totalPublished: contents.length,
byPlatform,
period,
};
}
}

View File

@@ -0,0 +1,9 @@
// Content Module Index
export * from './content.module';
export * from './content.service';
export * from './content.controller';
export * from './services/master-content.service';
export * from './services/building-blocks.service';
export * from './services/writing-styles.service';
export * from './services/content-variations.service';
export * from './services/platform-adapters.service';

View File

@@ -0,0 +1,193 @@
// Building Blocks Service - Extract reusable content elements
// Path: src/modules/content/services/building-blocks.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../../database/prisma.service';
export interface BuildingBlocks {
hooks: string[];
painPoints: string[];
paradoxes: string[];
quotes: string[];
statistics: string[];
callToActions: string[];
metaphors: string[];
stories: string[];
}
@Injectable()
export class BuildingBlocksService {
private readonly logger = new Logger(BuildingBlocksService.name);
constructor(private readonly prisma: PrismaService) { }
/**
* Extract building blocks from content/research
*/
async extract(title: string, researchNotes?: string): Promise<BuildingBlocks> {
this.logger.log(`Extracting building blocks for: ${title}`);
// AI-powered extraction will be added here
// For now, generate template blocks
return {
hooks: this.generateHooks(title),
painPoints: this.generatePainPoints(title),
paradoxes: this.generateParadoxes(title),
quotes: [],
statistics: [],
callToActions: this.generateCTAs(title),
metaphors: [],
stories: [],
};
}
/**
* Generate platform-specific content from building blocks
*/
async generatePlatformContent(
masterBody: string,
blocks: BuildingBlocks,
platform: string,
): Promise<string[]> {
const platformConfig = this.getPlatformConfig(platform);
const variations: string[] = [];
// Generate variations based on platform constraints
for (let i = 0; i < platformConfig.variationCount; i++) {
const hook = blocks.hooks[i % blocks.hooks.length];
const cta = blocks.callToActions[i % blocks.callToActions.length];
let content = '';
if (platform === 'TWITTER') {
content = this.generateTwitterContent(hook, masterBody, cta, platformConfig.maxLength);
} else if (platform === 'LINKEDIN') {
content = this.generateLinkedInContent(hook, masterBody, cta, platformConfig.maxLength);
} else if (platform === 'INSTAGRAM') {
content = this.generateInstagramContent(hook, masterBody, cta, platformConfig.maxLength);
} else {
content = this.generateGenericContent(hook, masterBody, cta, platformConfig.maxLength);
}
variations.push(content);
}
return variations;
}
/**
* Save building blocks for reuse
*/
async save(userId: string, masterContentId: string, blocks: BuildingBlocks) {
return this.prisma.buildingBlock.createMany({
data: [
...blocks.hooks.map((text) => ({
userId,
masterContentId,
type: 'HOOK' as const,
content: text,
})),
...blocks.painPoints.map((text) => ({
userId,
masterContentId,
type: 'PAIN_POINT' as const,
content: text,
})),
...blocks.callToActions.map((text) => ({
userId,
masterContentId,
type: 'CTA' as const,
content: text,
})),
],
});
}
/**
* Get saved building blocks by type
*/
async getByType(userId: string, type: string, limit = 20) {
return this.prisma.buildingBlock.findMany({
where: { masterContent: { userId }, type: type as any },
orderBy: { usageCount: 'desc' },
take: limit,
});
}
// Hook Templates
private generateHooks(title: string): string[] {
const templates = [
`The biggest mistake people make with ${title} is...`,
`What if everything you knew about ${title} was wrong?`,
`I spent 5 years studying ${title}. Here's what I learned:`,
`${title} doesn't work the way you think it does.`,
`The hidden truth about ${title} that nobody talks about:`,
`Why 95% of people fail at ${title} (and how to be in the 5%):`,
`Stop doing this one thing with ${title}. It's costing you.`,
`I was wrong about ${title}. Here's my updated take:`,
];
return templates;
}
// Pain Point Templates
private generatePainPoints(title: string): string[] {
return [
`Struggling to get results with ${title}?`,
`Feeling overwhelmed by all the ${title} advice out there?`,
`Tired of spending hours on ${title} with nothing to show for it?`,
`Frustrated that ${title} seems to work for everyone but you?`,
`Not sure where to start with ${title}?`,
];
}
// Paradox Templates
private generateParadoxes(title: string): string[] {
return [
`The more you focus on ${title}, the less progress you make.`,
`To master ${title}, you must first unlearn everything you know.`,
`The best ${title} strategy is having no strategy at all.`,
`Less effort in ${title} often leads to better results.`,
];
}
// CTA Templates
private generateCTAs(title: string): string[] {
return [
`Want to learn more about ${title}? Link in bio.`,
`Save this post for later. You'll need it.`,
`Drop a 🔥 if this changed your perspective on ${title}.`,
`Follow for more insights on ${title}.`,
`Share this with someone who needs to hear it.`,
`Comment "INFO" and I'll send you my complete ${title} guide.`,
];
}
// Platform configurations
private getPlatformConfig(platform: string) {
const configs: Record<string, { maxLength: number; variationCount: number }> = {
TWITTER: { maxLength: 280, variationCount: 5 },
LINKEDIN: { maxLength: 3000, variationCount: 3 },
INSTAGRAM: { maxLength: 2200, variationCount: 3 },
FACEBOOK: { maxLength: 63206, variationCount: 2 },
TIKTOK: { maxLength: 300, variationCount: 3 },
};
return configs[platform] || { maxLength: 1000, variationCount: 2 };
}
// Platform-specific generators
private generateTwitterContent(hook: string, body: string, cta: string, maxLength: number): string {
const content = `${hook}\n\n${body.substring(0, 150)}...\n\n${cta}`;
return content.substring(0, maxLength);
}
private generateLinkedInContent(hook: string, body: string, cta: string, maxLength: number): string {
return `${hook}\n\n${body.substring(0, 2500)}\n\n---\n${cta}`;
}
private generateInstagramContent(hook: string, body: string, cta: string, maxLength: number): string {
return `${hook}\n\n${body.substring(0, 1800)}\n\n.\n.\n.\n${cta}`;
}
private generateGenericContent(hook: string, body: string, cta: string, maxLength: number): string {
return `${hook}\n\n${body}\n\n${cta}`.substring(0, maxLength);
}
}

View File

@@ -0,0 +1,168 @@
// Content Variations Service - Generate multiple content variations
// Path: src/modules/content/services/content-variations.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../../database/prisma.service';
import { ContentVariant } from '@prisma/client';
export interface VariationConfig {
count: number;
strategy: 'ab_test' | 'audience_segment' | 'platform_specific' | 'time_based';
variations: VariationSpec[];
}
export interface VariationSpec {
name: string;
hook?: string;
tone?: string;
length?: 'shorter' | 'same' | 'longer';
cta?: string;
targetAudience?: string;
}
@Injectable()
export class ContentVariationsService {
private readonly logger = new Logger(ContentVariationsService.name);
constructor(private readonly prisma: PrismaService) { }
/**
* Generate content variations for A/B testing
*/
async generateVariations(
contentId: string,
config: VariationConfig,
): Promise<any[]> {
const content = await this.prisma.content.findUnique({
where: { id: contentId },
});
if (!content) {
throw new Error('Content not found');
}
const variations: ContentVariant[] = [];
for (const spec of config.variations) {
const variation = await this.createVariation(content, spec);
variations.push(variation);
}
return variations;
}
/**
* Create a single variation
*/
private async createVariation(
originalContent: any,
spec: VariationSpec,
) {
let variedBody = originalContent.body || '';
// Apply hook variation
if (spec.hook) {
variedBody = this.replaceHook(variedBody, spec.hook);
}
// Apply length variation
if (spec.length === 'shorter') {
variedBody = this.shortenContent(variedBody, 0.7);
} else if (spec.length === 'longer') {
variedBody = this.expandContent(variedBody, 1.3);
}
// Apply CTA variation
if (spec.cta) {
variedBody = this.replaceCTA(variedBody, spec.cta);
}
// Save variation
return this.prisma.contentVariant.create({
data: {
contentId: originalContent.id,
name: spec.name,
text: variedBody,
platform: originalContent.platform,
isActive: true,
},
});
}
/**
* Get variation performance metrics
*/
async getPerformanceMetrics(contentId: string) {
const variants = await this.prisma.contentVariant.findMany({
where: { contentId },
select: {
id: true,
name: true,
impressions: true,
clicks: true,
engagements: true,
shares: true,
conversions: true,
},
});
return variants.map((v) => ({
...v,
ctr: v.impressions > 0 ? (v.clicks / v.impressions) * 100 : 0,
engagementRate: v.impressions > 0 ? (v.engagements / v.impressions) * 100 : 0,
conversionRate: v.clicks > 0 ? (v.conversions / v.clicks) * 100 : 0,
}));
}
/**
* Select best performing variation
*/
async selectWinner(contentId: string, metric: 'ctr' | 'engagementRate' | 'conversionRate') {
const metrics = await this.getPerformanceMetrics(contentId);
if (metrics.length === 0) {
return null;
}
// Sort by selected metric
metrics.sort((a, b) => b[metric] - a[metric]);
const winner = metrics[0];
// Mark as winner
await this.prisma.contentVariant.update({
where: { id: winner.id },
data: { isWinner: true },
});
return winner;
}
// Helper methods
private replaceHook(content: string, newHook: string): string {
const lines = content.split('\n');
if (lines.length > 0) {
lines[0] = newHook;
}
return lines.join('\n');
}
private shortenContent(content: string, ratio: number): string {
const words = content.split(/\s+/);
const targetLength = Math.floor(words.length * ratio);
return words.slice(0, targetLength).join(' ') + '...';
}
private expandContent(content: string, ratio: number): string {
// In real implementation, this would use AI to expand
return content + '\n\n[Expanded content would be added here]';
}
private replaceCTA(content: string, newCTA: string): string {
const lines = content.split('\n');
if (lines.length > 0) {
lines[lines.length - 1] = newCTA;
}
return lines.join('\n');
}
}

View File

@@ -0,0 +1,268 @@
// Master Content Service - Hub for long-form content creation
// Path: src/modules/content/services/master-content.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../../database/prisma.service';
import { BuildingBlocksService } from './building-blocks.service';
import { WritingStylesService } from './writing-styles.service';
import { MasterContentType, ContentLanguage } from '@prisma/client';
export interface CreateMasterContentDto {
title: string;
type: MasterContentType;
nicheId?: string;
trendId?: string;
researchNotes?: string;
targetAudience?: string;
writingStyleId?: string;
targetLanguage?: ContentLanguage;
}
export interface GeneratedMasterContent {
id: string;
title: string;
body: string;
outline: string[];
buildingBlocks: {
hooks: string[];
painPoints: string[];
paradoxes: string[];
quotes: string[];
statistics: string[];
};
metadata: {
wordCount: number;
readingTime: number;
seoScore?: number;
};
}
@Injectable()
export class MasterContentService {
private readonly logger = new Logger(MasterContentService.name);
constructor(
private readonly prisma: PrismaService,
private readonly buildingBlocks: BuildingBlocksService,
private readonly writingStyles: WritingStylesService,
) { }
/**
* Create master content (Blog, Newsletter, Podcast Script, Video Script)
*/
async create(
userId: string,
data: CreateMasterContentDto,
): Promise<GeneratedMasterContent> {
this.logger.log(`Creating ${data.type} master content: ${data.title}`);
// Get writing style
const style = data.writingStyleId
? await this.writingStyles.getById(data.writingStyleId)
: await this.writingStyles.getDefault(userId);
// Generate outline based on content type
const outline = this.generateOutline(data.type, data.title);
// Create master content record
const masterContent = await this.prisma.masterContent.create({
data: {
userId,
nicheId: data.nicheId,
trendId: data.trendId,
title: data.title,
type: data.type,
researchNotes: data.researchNotes,
targetAudience: data.targetAudience,
writingStyleId: style?.id,
outline,
status: 'DRAFT',
body: '',
},
});
// Extract building blocks
const blocks = await this.buildingBlocks.extract(data.title, data.researchNotes);
// Generate content body (placeholder - will integrate with AI)
const body = this.generateContentBody(data.type, outline, blocks, style);
// Update with generated content
await this.prisma.masterContent.update({
where: { id: masterContent.id },
data: {
body,
hooks: blocks.hooks,
painPoints: blocks.painPoints,
paradoxes: blocks.paradoxes,
quotes: blocks.quotes,
},
});
return {
id: masterContent.id,
title: data.title,
body,
outline,
buildingBlocks: blocks,
metadata: {
wordCount: body.split(/\s+/).length,
readingTime: Math.ceil(body.split(/\s+/).length / 200),
},
};
}
/**
* Get master content by ID
*/
async getById(id: string) {
return this.prisma.masterContent.findUnique({
where: { id },
include: {
niche: true,
trend: true,
writingStyle: true,
contents: true,
buildingBlocks: true,
},
});
}
/**
* Get user's master contents
*/
async getByUser(
userId: string,
options?: {
type?: MasterContentType;
limit?: number;
offset?: number;
},
) {
return this.prisma.masterContent.findMany({
where: {
userId,
...(options?.type && { type: options.type }),
},
orderBy: { createdAt: 'desc' },
take: options?.limit || 20,
skip: options?.offset || 0,
include: {
niche: { select: { id: true, name: true } },
_count: { select: { contents: true } },
},
});
}
/**
* Repurpose master content into multiple pieces
*/
async repurpose(masterContentId: string, platforms: string[]) {
const masterContent = await this.getById(masterContentId);
if (!masterContent) {
throw new Error('Master content not found');
}
const blocks = {
hooks: masterContent.buildingBlocks.filter(b => b.type === 'HOOK').map(b => b.content),
painPoints: masterContent.buildingBlocks.filter(b => b.type === 'PAIN_POINT').map(b => b.content),
paradoxes: masterContent.buildingBlocks.filter(b => b.type === 'PARADOX').map(b => b.content),
quotes: masterContent.buildingBlocks.filter(b => b.type === 'QUOTE').map(b => b.content),
statistics: masterContent.buildingBlocks.filter(b => b.type === 'STATISTIC').map(b => b.content),
callToActions: masterContent.buildingBlocks.filter(b => b.type === 'CTA').map(b => b.content),
metaphors: masterContent.buildingBlocks.filter(b => b.type === 'METAPHOR').map(b => b.content),
stories: masterContent.buildingBlocks.filter(b => b.type === 'STORY').map(b => b.content),
};
// Generate content for each platform
const results: { platform: string; variations: string[] }[] = [];
for (const platform of platforms) {
const variations = await this.buildingBlocks.generatePlatformContent(
masterContent.body || '',
blocks,
platform,
);
results.push({ platform, variations });
}
return results;
}
/**
* Generate outline based on content type
*/
private generateOutline(type: MasterContentType, title: string): string[] {
const outlines: Record<MasterContentType, string[]> = {
BLOG: [
'Introduction - Hook & Problem Statement',
'Background - Context & Why It Matters',
'Main Point 1 - Key Insight',
'Main Point 2 - Supporting Evidence',
'Main Point 3 - Practical Application',
'Objections & Counterarguments',
'Conclusion - Call to Action',
],
NEWSLETTER: [
'Attention-Grabbing Opening',
'This Week\'s Key Insight',
'Quick Win / Actionable Tip',
'Deep Dive Section',
'Resource of the Week',
'Community Highlight',
'What\'s Coming Next',
],
PODCAST_SCRIPT: [
'Cold Open / Teaser',
'Intro & Welcome',
'Main Topic Introduction',
'Segment 1 - Story/Context',
'Segment 2 - Key Insights',
'Segment 3 - Practical Takeaways',
'Sponsor Break (if applicable)',
'Listener Q&A / Community',
'Outro & CTA',
],
VIDEO_SCRIPT: [
'Hook (First 5 seconds)',
'Promise / What They\'ll Learn',
'Credibility Moment',
'Main Content Section 1',
'Main Content Section 2',
'Main Content Section 3',
'Recap & Summary',
'Call to Action',
'End Screen Prompt',
],
THREAD: [
'Hook Tweet (Pattern Interrupt)',
'Context / Problem',
'Key Insight 1',
'Key Insight 2',
'Key Insight 3',
'Proof / Example',
'Summary',
'CTA',
],
};
return outlines[type] || outlines.BLOG;
}
/**
* Generate content body (placeholder for AI integration)
*/
private generateContentBody(
type: MasterContentType,
outline: string[],
blocks: any,
style: any,
): string {
// This will be replaced with actual AI generation
const sections = outline.map((section, index) => {
const hook = (blocks.hooks && blocks.hooks.length > 0) ? blocks.hooks[index % blocks.hooks.length] : '';
return `## ${section}\n\n${hook}\n\n[Content for ${section} will be generated here]`;
});
return sections.join('\n\n---\n\n');
}
}

View File

@@ -0,0 +1,288 @@
// Platform Adapters Service - Transform content for different platforms
// Path: src/modules/content/services/platform-adapters.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { SocialPlatform } from '@prisma/client';
export interface PlatformConfig {
name: string;
maxLength: number;
supportsMedia: boolean;
supportsLinks: boolean;
supportsHashtags: boolean;
supportsMentions: boolean;
supportsEmoji: boolean;
mediaFormats?: string[];
optimalLength?: number;
bestPostingTimes?: string[];
engagementTips?: string[];
}
export const PLATFORM_CONFIGS: Record<SocialPlatform, PlatformConfig> = {
TWITTER: {
name: 'X (Twitter)',
maxLength: 280,
optimalLength: 240,
supportsMedia: true,
supportsLinks: true,
supportsHashtags: true,
supportsMentions: true,
supportsEmoji: true,
mediaFormats: ['image', 'video', 'gif'],
bestPostingTimes: ['9:00', '12:00', '17:00', '21:00'],
engagementTips: [
'Use threads for longer content',
'Ask questions to boost engagement',
'Quote tweet with commentary',
],
},
LINKEDIN: {
name: 'LinkedIn',
maxLength: 3000,
optimalLength: 1300,
supportsMedia: true,
supportsLinks: true,
supportsHashtags: true,
supportsMentions: true,
supportsEmoji: true,
mediaFormats: ['image', 'video', 'document', 'carousel'],
bestPostingTimes: ['7:30', '12:00', '17:00'],
engagementTips: [
'Hook in first line (before "see more")',
'Use line breaks for readability',
'End with a question',
],
},
INSTAGRAM: {
name: 'Instagram',
maxLength: 2200,
optimalLength: 1500,
supportsMedia: true,
supportsLinks: false,
supportsHashtags: true,
supportsMentions: true,
supportsEmoji: true,
mediaFormats: ['image', 'video', 'reels', 'carousel', 'stories'],
bestPostingTimes: ['6:00', '11:00', '19:00'],
engagementTips: [
'Use up to 30 hashtags',
'Put hashtags in first comment',
'Include CTA in caption',
],
},
FACEBOOK: {
name: 'Facebook',
maxLength: 63206,
optimalLength: 250,
supportsMedia: true,
supportsLinks: true,
supportsHashtags: true,
supportsMentions: true,
supportsEmoji: true,
mediaFormats: ['image', 'video', 'reels', 'stories'],
bestPostingTimes: ['9:00', '13:00', '19:00'],
engagementTips: [
'Shorter posts perform better',
'Native video preferred',
'Use Facebook Live for engagement',
],
},
TIKTOK: {
name: 'TikTok',
maxLength: 300,
optimalLength: 150,
supportsMedia: true,
supportsLinks: false,
supportsHashtags: true,
supportsMentions: true,
supportsEmoji: true,
mediaFormats: ['video'],
bestPostingTimes: ['7:00', '10:00', '19:00', '23:00'],
engagementTips: [
'Hook in first 3 seconds',
'Use trending sounds',
'Keep videos under 60 seconds',
],
},
YOUTUBE: {
name: 'YouTube',
maxLength: 5000,
optimalLength: 500,
supportsMedia: true,
supportsLinks: true,
supportsHashtags: true,
supportsMentions: false,
supportsEmoji: true,
mediaFormats: ['video', 'shorts'],
bestPostingTimes: ['12:00', '15:00', '19:00'],
engagementTips: [
'Use timestamps in description',
'Include relevant keywords',
'Add cards and end screens',
],
},
PINTEREST: {
name: 'Pinterest',
maxLength: 500,
optimalLength: 300,
supportsMedia: true,
supportsLinks: true,
supportsHashtags: true,
supportsMentions: false,
supportsEmoji: true,
mediaFormats: ['image', 'video', 'idea_pin'],
bestPostingTimes: ['20:00', '21:00', '22:00'],
engagementTips: [
'Vertical images perform best',
'Use keyword-rich descriptions',
'Create multiple pins per post',
],
},
THREADS: {
name: 'Threads',
maxLength: 500,
optimalLength: 280,
supportsMedia: true,
supportsLinks: false,
supportsHashtags: false,
supportsMentions: true,
supportsEmoji: true,
mediaFormats: ['image', 'video'],
bestPostingTimes: ['12:00', '17:00', '21:00'],
engagementTips: [
'Conversational tone works best',
'Reply to trending topics',
'Cross-post from Instagram',
],
},
};
@Injectable()
export class PlatformAdaptersService {
private readonly logger = new Logger(PlatformAdaptersService.name);
/**
* Get platform configuration
*/
getConfig(platform: SocialPlatform): PlatformConfig {
return PLATFORM_CONFIGS[platform];
}
/**
* Get all platform configs
*/
getAllConfigs(): Record<SocialPlatform, PlatformConfig> {
return PLATFORM_CONFIGS;
}
/**
* Adapt content for specific platform
*/
adapt(content: string, sourcePlatform: SocialPlatform, targetPlatform: SocialPlatform): string {
const sourceConfig = PLATFORM_CONFIGS[sourcePlatform];
const targetConfig = PLATFORM_CONFIGS[targetPlatform];
let adapted = content;
// Handle length constraints
if (adapted.length > targetConfig.maxLength) {
adapted = this.truncate(adapted, targetConfig.maxLength);
}
// Handle link support
if (!targetConfig.supportsLinks && sourceConfig.supportsLinks) {
adapted = this.removeLinks(adapted);
}
// Handle hashtag support
if (!targetConfig.supportsHashtags && sourceConfig.supportsHashtags) {
adapted = this.removeHashtags(adapted);
}
return adapted;
}
/**
* Format content for platform
*/
format(content: string, platform: SocialPlatform): string {
const config = PLATFORM_CONFIGS[platform];
switch (platform) {
case 'TWITTER':
return this.formatForTwitter(content, config);
case 'LINKEDIN':
return this.formatForLinkedIn(content, config);
case 'INSTAGRAM':
return this.formatForInstagram(content, config);
default:
return this.truncate(content, config.maxLength);
}
}
/**
* Check if content is valid for platform
*/
validate(content: string, platform: SocialPlatform): { valid: boolean; issues: string[] } {
const config = PLATFORM_CONFIGS[platform];
const issues: string[] = [];
if (content.length > config.maxLength) {
issues.push(`Content exceeds ${config.maxLength} character limit`);
}
if (!config.supportsLinks && this.containsLinks(content)) {
issues.push('Platform does not support links in captions');
}
return { valid: issues.length === 0, issues };
}
// Private helpers
private truncate(content: string, maxLength: number): string {
if (content.length <= maxLength) return content;
return content.substring(0, maxLength - 3) + '...';
}
private removeLinks(content: string): string {
return content.replace(/https?:\/\/[^\s]+/g, '[link in bio]');
}
private removeHashtags(content: string): string {
return content.replace(/#\w+/g, '').replace(/\s+/g, ' ').trim();
}
private containsLinks(content: string): boolean {
return /https?:\/\/[^\s]+/.test(content);
}
private formatForTwitter(content: string, config: PlatformConfig): string {
let formatted = content;
// Ensure hashtags at end
const hashtags = formatted.match(/#\w+/g) || [];
formatted = formatted.replace(/#\w+/g, '').trim();
formatted = `${formatted}\n\n${hashtags.slice(0, 3).join(' ')}`;
return this.truncate(formatted, config.maxLength);
}
private formatForLinkedIn(content: string, config: PlatformConfig): string {
// Add line breaks for readability
const lines = content.split(/(?<=[.!?])\s+/);
return lines.slice(0, 20).join('\n\n');
}
private formatForInstagram(content: string, config: PlatformConfig): string {
let formatted = content;
// Add dots before hashtags (common Instagram style)
if (formatted.includes('#')) {
const mainContent = formatted.split('#')[0].trim();
const hashtags = formatted.match(/#\w+/g) || [];
formatted = `${mainContent}\n.\n.\n.\n${hashtags.join(' ')}`;
}
return formatted;
}
}

View File

@@ -0,0 +1,424 @@
// Writing Styles Service - Extended with 15+ personality tones
// Path: src/modules/content/services/writing-styles.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../../database/prisma.service';
import { ContentLanguage } from '@prisma/client';
export interface WritingStyleConfig {
name: string;
description?: string;
tone: WritingTone;
voice: 'first_person' | 'second_person' | 'third_person';
vocabulary: 'simple' | 'intermediate' | 'advanced' | 'technical';
sentenceLength: 'short' | 'medium' | 'long' | 'varied';
emojiUsage: 'none' | 'minimal' | 'moderate' | 'heavy';
hashtagStyle: 'none' | 'minimal' | 'topic_based' | 'trending';
structurePreference: 'paragraphs' | 'bullets' | 'numbered' | 'mixed';
engagementStyle: 'educational' | 'storytelling' | 'data_driven' | 'conversational' | 'provocative';
signatureElements?: string[];
avoidPhrases?: string[];
preferredPhrases?: string[];
language?: ContentLanguage;
}
// 15+ Writing Tones
export type WritingTone =
| 'storyteller' // Narrative-driven, emotionally engaging
| 'narrator' // Documentary style, observational
| 'sarcastic' // Witty, ironic, sharp humor
| 'inspirational' // Motivational, uplifting
| 'professional' // Business-like, polished
| 'casual' // Relaxed, conversational
| 'friendly' // Warm, approachable
| 'authoritative' // Expert, commanding
| 'playful' // Fun, light-hearted
| 'provocative' // Controversial, challenging
| 'empathetic' // Understanding, supportive
| 'analytical' // Data-focused, logical
| 'humorous' // Comedy-driven, entertaining
| 'minimalist' // Concise, direct
| 'dramatic' // Intense, emotional
| 'educational'; // Teaching, informative
export const WRITING_TONES: Record<WritingTone, { emoji: string; description: string; promptHint: string }> = {
storyteller: {
emoji: '📖',
description: 'Narrative-driven, weaves stories to make points memorable',
promptHint: 'Write like a master storyteller, using narrative techniques, character arcs, and emotional hooks',
},
narrator: {
emoji: '🎙️',
description: 'Documentary-style, observational and descriptive',
promptHint: 'Write like a documentary narrator, observing and describing with clarity and depth',
},
sarcastic: {
emoji: '😏',
description: 'Witty, ironic, with sharp humor',
promptHint: 'Write with sarcastic wit, using irony and clever observations that make readers think',
},
inspirational: {
emoji: '✨',
description: 'Motivational and uplifting content',
promptHint: 'Write to inspire and motivate, using powerful language that uplifts the reader',
},
professional: {
emoji: '💼',
description: 'Business-like, polished and authoritative',
promptHint: 'Write in a professional, business-appropriate tone with credibility and expertise',
},
casual: {
emoji: '😊',
description: 'Relaxed and conversational',
promptHint: 'Write casually, like talking to a friend over coffee',
},
friendly: {
emoji: '🤝',
description: 'Warm, approachable and welcoming',
promptHint: 'Write in a warm, friendly manner that makes readers feel comfortable and welcomed',
},
authoritative: {
emoji: '👔',
description: 'Expert voice with commanding presence',
promptHint: 'Write with authority and expertise, establishing credibility and trust',
},
playful: {
emoji: '🎉',
description: 'Fun, light-hearted and entertaining',
promptHint: 'Write playfully with humor, wordplay, and a light touch',
},
provocative: {
emoji: '🔥',
description: 'Controversial, thought-provoking',
promptHint: 'Write to challenge assumptions and provoke thought, with bold statements',
},
empathetic: {
emoji: '💙',
description: 'Understanding and supportive',
promptHint: 'Write with empathy, acknowledging struggles and offering understanding',
},
analytical: {
emoji: '📊',
description: 'Data-focused and logical',
promptHint: 'Write analytically, using data, logic, and structured arguments',
},
humorous: {
emoji: '😂',
description: 'Comedy-driven, entertaining',
promptHint: 'Write with humor, jokes, and entertainment value',
},
minimalist: {
emoji: '🎯',
description: 'Concise, direct, no fluff',
promptHint: 'Write minimally, every word counts, eliminate all unnecessary words',
},
dramatic: {
emoji: '🎭',
description: 'Intense, emotional, theatrical',
promptHint: 'Write dramatically with intensity, building tension and emotional impact',
},
educational: {
emoji: '📚',
description: 'Teaching-focused, informative',
promptHint: 'Write to educate, explain concepts clearly with examples and structure',
},
};
// Preset Writing Styles (combining tone + other settings)
export const PRESET_STYLES: Record<string, WritingStyleConfig> = {
master_storyteller: {
name: 'Master Storyteller',
description: 'Narrative-driven, emotionally engaging content with story arcs',
tone: 'storyteller',
voice: 'first_person',
vocabulary: 'intermediate',
sentenceLength: 'varied',
emojiUsage: 'minimal',
hashtagStyle: 'none',
structurePreference: 'paragraphs',
engagementStyle: 'storytelling',
preferredPhrases: ['Let me tell you...', 'Picture this:', 'Here\'s what happened:'],
},
sharp_sarcastic: {
name: 'Sharp & Sarcastic',
description: 'Witty observations with ironic humor',
tone: 'sarcastic',
voice: 'first_person',
vocabulary: 'intermediate',
sentenceLength: 'short',
emojiUsage: 'minimal',
hashtagStyle: 'none',
structurePreference: 'paragraphs',
engagementStyle: 'provocative',
preferredPhrases: ['Oh sure,', 'Because obviously,', 'Shocking, I know.'],
avoidPhrases: ['To be honest', 'Actually'],
},
documentary_narrator: {
name: 'Documentary Narrator',
description: 'Observational, descriptive, cinematic',
tone: 'narrator',
voice: 'third_person',
vocabulary: 'advanced',
sentenceLength: 'long',
emojiUsage: 'none',
hashtagStyle: 'none',
structurePreference: 'paragraphs',
engagementStyle: 'storytelling',
},
motivational_coach: {
name: 'Motivational Coach',
description: 'Inspiring, action-oriented, empowering',
tone: 'inspirational',
voice: 'second_person',
vocabulary: 'simple',
sentenceLength: 'short',
emojiUsage: 'moderate',
hashtagStyle: 'minimal',
structurePreference: 'bullets',
engagementStyle: 'conversational',
preferredPhrases: ['You can do this!', 'Here\'s the truth:', 'Your time is now.'],
},
data_analyst: {
name: 'Data Analyst',
description: 'Facts, figures, and logical conclusions',
tone: 'analytical',
voice: 'first_person',
vocabulary: 'technical',
sentenceLength: 'medium',
emojiUsage: 'none',
hashtagStyle: 'topic_based',
structurePreference: 'numbered',
engagementStyle: 'data_driven',
preferredPhrases: ['The data shows:', 'Research indicates:', 'Here are the numbers:'],
},
friendly_teacher: {
name: 'Friendly Teacher',
description: 'Educational, patient, encouraging',
tone: 'educational',
voice: 'second_person',
vocabulary: 'simple',
sentenceLength: 'short',
emojiUsage: 'moderate',
hashtagStyle: 'topic_based',
structurePreference: 'numbered',
engagementStyle: 'educational',
preferredPhrases: ['Let me explain:', 'Think of it this way:', 'Here\'s a simple example:'],
},
corporate_executive: {
name: 'Corporate Executive',
description: 'Professional, strategic, leadership-focused',
tone: 'professional',
voice: 'first_person',
vocabulary: 'advanced',
sentenceLength: 'medium',
emojiUsage: 'none',
hashtagStyle: 'topic_based',
structurePreference: 'mixed',
engagementStyle: 'data_driven',
},
stand_up_comedian: {
name: 'Stand-up Comedian',
description: 'Funny, self-deprecating, observational humor',
tone: 'humorous',
voice: 'first_person',
vocabulary: 'simple',
sentenceLength: 'varied',
emojiUsage: 'moderate',
hashtagStyle: 'none',
structurePreference: 'paragraphs',
engagementStyle: 'conversational',
},
thought_provocateur: {
name: 'Thought Provocateur',
description: 'Bold statements, contrarian views, challenges assumptions',
tone: 'provocative',
voice: 'first_person',
vocabulary: 'advanced',
sentenceLength: 'varied',
emojiUsage: 'none',
hashtagStyle: 'none',
structurePreference: 'paragraphs',
engagementStyle: 'provocative',
preferredPhrases: ['Unpopular opinion:', 'Hot take:', 'Everyone is wrong about:'],
},
zen_minimalist: {
name: 'Zen Minimalist',
description: 'Every word matters, no fluff, pure clarity',
tone: 'minimalist',
voice: 'second_person',
vocabulary: 'simple',
sentenceLength: 'short',
emojiUsage: 'none',
hashtagStyle: 'none',
structurePreference: 'bullets',
engagementStyle: 'educational',
},
};
@Injectable()
export class WritingStylesService {
private readonly logger = new Logger(WritingStylesService.name);
constructor(private readonly prisma: PrismaService) { }
/**
* Get all available tones
*/
getTones(): typeof WRITING_TONES {
return WRITING_TONES;
}
/**
* Get all preset styles
*/
getPresets(): typeof PRESET_STYLES {
return PRESET_STYLES;
}
/**
* Create a custom writing style
*/
async create(userId: string, config: WritingStyleConfig) {
return this.prisma.writingStyle.create({
data: {
userId,
name: config.name,
type: 'CUSTOM',
tone: config.tone,
vocabulary: Array.isArray(config.vocabulary) ? config.vocabulary : [config.vocabulary],
sentenceLength: config.sentenceLength,
emojiUsage: config.emojiUsage,
hashtagStyle: config.hashtagStyle,
structurePreference: config.structurePreference,
engagementStyle: config.engagementStyle,
signatureElements: config.signatureElements || [],
avoidWords: config.avoidPhrases || [],
preferredPhrases: config.preferredPhrases || [],
isDefault: false,
},
});
}
/**
* Get writing style by ID
*/
async getById(id: string) {
// Check if it's a preset
if (id.startsWith('preset-')) {
const presetKey = id.replace('preset-', '');
return PRESET_STYLES[presetKey] ? { id, ...PRESET_STYLES[presetKey] } : null;
}
return this.prisma.writingStyle.findUnique({
where: { id },
});
}
/**
* Get user's default writing style
*/
async getDefault(userId: string) {
const userDefault = await this.prisma.writingStyle.findFirst({
where: { userId, isDefault: true },
});
return userDefault || { id: 'preset-master_storyteller', ...PRESET_STYLES.master_storyteller };
}
/**
* Get all user's writing styles (custom + presets)
*/
async getAll(userId: string) {
const customStyles = await this.prisma.writingStyle.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
});
const presets = Object.entries(PRESET_STYLES).map(([key, style]) => ({
id: `preset-${key}`,
...style,
isPreset: true,
}));
return [...customStyles, ...presets];
}
/**
* Set default writing style
*/
async setDefault(userId: string, styleId: string) {
await this.prisma.writingStyle.updateMany({
where: { userId, isDefault: true },
data: { isDefault: false },
});
return this.prisma.writingStyle.update({
where: { id: styleId },
data: { isDefault: true },
});
}
/**
* Generate AI prompt for style
*/
generatePrompt(style: WritingStyleConfig, language?: ContentLanguage): string {
const toneInfo = WRITING_TONES[style.tone];
let prompt = `
WRITING STYLE INSTRUCTIONS:
${toneInfo.promptHint}
STYLE PARAMETERS:
- Tone: ${style.tone} (${toneInfo.description})
- Voice: ${style.voice.replace('_', ' ')}
- Vocabulary: ${style.vocabulary}
- Sentence length: ${style.sentenceLength}
- Emoji usage: ${style.emojiUsage}
- Structure: ${style.structurePreference}
- Engagement: ${style.engagementStyle}
`;
if (style.preferredPhrases?.length) {
prompt += `\n- Use phrases like: ${style.preferredPhrases.join(', ')}`;
}
if (style.avoidPhrases?.length) {
prompt += `\n- Avoid phrases: ${style.avoidPhrases.join(', ')}`;
}
if (language) {
prompt += `\n\nWRITE CONTENT IN: ${language}`;
}
return prompt.trim();
}
/**
* Apply writing style transformations
*/
applyStyle(content: string, style: WritingStyleConfig): string {
let styledContent = content;
if (style.emojiUsage === 'none') {
styledContent = styledContent.replace(/[\u{1F300}-\u{1F9FF}]/gu, '');
}
if (style.structurePreference === 'bullets') {
styledContent = this.convertToBullets(styledContent);
} else if (style.structurePreference === 'numbered') {
styledContent = this.convertToNumbered(styledContent);
}
return styledContent;
}
private convertToBullets(content: string): string {
const sentences = content.split(/(?<=[.!?])\s+/);
return sentences.map((s) => `${s.trim()}`).join('\n');
}
private convertToNumbered(content: string): string {
const sentences = content.split(/(?<=[.!?])\s+/);
return sentences.map((s, i) => `${i + 1}. ${s.trim()}`).join('\n');
}
}