generated from fahricansecer/boilerplate-be
425 lines
15 KiB
TypeScript
425 lines
15 KiB
TypeScript
// 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');
|
|
}
|
|
}
|