@bernierllc/ai-content-generator
Version:
AI-powered content generation service for social posts, blog articles, emails, and multi-platform content
865 lines (864 loc) • 32.2 kB
JavaScript
/*
Copyright (c) 2025 Bernier LLC
This file is licensed to the client under a limited-use license.
The client may use and modify this code *only within the scope of the project it was delivered for*.
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
*/
/**
* AI Content Generator Service
*
* Generates AI-powered content for social media posts, blog articles,
* emails, and multi-platform content from source material.
*/
export class AIContentGenerator {
provider;
fallbackProvider;
config;
analyticsData = [];
constructor(config) {
this.config = config;
this.provider = config.provider;
this.fallbackProvider = config.fallbackProvider;
}
// ============================================
// SOCIAL MEDIA GENERATION
// ============================================
/**
* Generate a social media post from source content
*/
async generateSocialPost(sourceContent, platform, options) {
const startTime = Date.now();
// Validate source content
if (!sourceContent || sourceContent.trim().length === 0) {
return {
success: false,
error: 'Source content cannot be empty',
content: '',
};
}
const prompt = this.buildSocialPrompt(sourceContent, platform, options);
const temperature = options?.creativity ?? this.config.defaultTemperature ?? 0.7;
try {
const response = await this.executeWithFallback({
messages: [
{
role: 'system',
content: `You are an expert social media content creator specializing in ${platform}. Create engaging, platform-appropriate content.`,
},
{ role: 'user', content: prompt },
],
temperature,
maxTokens: this.getPlatformMaxTokens(platform),
});
if (!response.success || !response.content) {
return {
success: false,
error: response.error || 'Generation failed',
content: '',
};
}
const generatedContent = this.parsePlatformContent(response.content, platform);
// Generate alternate versions if requested
let alternateVersions;
if (options?.alternateVersions && options.alternateVersions > 0) {
alternateVersions = await this.generateAlternateVersions(sourceContent, platform, options, options.alternateVersions);
}
const result = {
success: true,
content: generatedContent,
metadata: {
platform,
generatedAt: new Date(),
model: this.provider.getProviderName(),
promptTokens: response.usage?.promptTokens || 0,
completionTokens: response.usage?.completionTokens || 0,
totalTokens: response.usage?.totalTokens || 0,
durationMs: Date.now() - startTime,
characterCount: generatedContent.length,
},
alternateVersions,
};
this.trackGeneration('social', platform, result.metadata?.totalTokens || 0);
return result;
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Generation failed',
content: '',
};
}
}
/**
* Generate posts for multiple platforms from a single source
*/
async generateMultiPlatformContent(sourceContent, platforms, options) {
const results = await Promise.all(platforms.map(async (platform) => ({
platform,
result: await this.generateSocialPost(sourceContent, platform, options),
})));
return {
success: true,
results,
totalGenerated: platforms.length,
successCount: results.filter((r) => r.result.success).length,
failureCount: results.filter((r) => !r.result.success).length,
metadata: {
sourceLength: sourceContent.length,
platforms,
generatedAt: new Date(),
},
};
}
// ============================================
// BLOG GENERATION
// ============================================
/**
* Generate a blog post from an outline
*/
async generateBlogPost(outline, options) {
const startTime = Date.now();
// Validate outline
if (!outline.topic || outline.topic.trim().length === 0) {
return {
success: false,
error: 'Blog topic cannot be empty',
content: '',
};
}
if (!outline.sections || outline.sections.length === 0) {
return {
success: false,
error: 'Blog outline must have at least one section',
content: '',
};
}
const prompt = this.buildBlogPrompt(outline, options);
const temperature = options?.creativity ?? this.config.defaultTemperature ?? 0.6;
try {
const response = await this.executeWithFallback({
messages: [
{
role: 'system',
content: 'You are an expert blog writer creating engaging, informative content.',
},
{ role: 'user', content: prompt },
],
temperature,
maxTokens: options?.targetLength ? Math.ceil(options.targetLength * 1.5) : 4000,
});
if (!response.success || !response.content) {
return {
success: false,
error: response.error || 'Blog generation failed',
content: '',
};
}
const result = {
success: true,
content: response.content,
metadata: {
platform: 'blog',
generatedAt: new Date(),
model: this.provider.getProviderName(),
promptTokens: response.usage?.promptTokens || 0,
completionTokens: response.usage?.completionTokens || 0,
totalTokens: response.usage?.totalTokens || 0,
durationMs: Date.now() - startTime,
wordCount: this.countWords(response.content),
},
};
this.trackGeneration('blog', undefined, result.metadata?.totalTokens || 0);
return result;
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Blog generation failed',
content: '',
};
}
}
/**
* Expand short content into a full blog post
*/
async expandToBlogPost(shortContent, targetLength = 1000, options) {
return this.generateBlogPost({
topic: 'Expansion of content',
sections: [
{
title: 'Main Content',
keyPoints: [shortContent],
},
],
targetAudience: options?.targetAudience || 'general',
tone: options?.tone || 'informative',
}, {
...options,
targetLength,
});
}
// ============================================
// EMAIL GENERATION
// ============================================
/**
* Generate an email from source content
*/
async generateEmail(sourceContent, emailType, options) {
const startTime = Date.now();
// Validate source content
if (!sourceContent || sourceContent.trim().length === 0) {
return {
success: false,
error: 'Source content cannot be empty',
content: '',
};
}
const prompt = this.buildEmailPrompt(sourceContent, emailType, options);
try {
const response = await this.executeWithFallback({
messages: [
{
role: 'system',
content: `You are an expert email copywriter specializing in ${emailType} emails. Create compelling, action-oriented content.`,
},
{ role: 'user', content: prompt },
],
temperature: 0.6,
maxTokens: 2000,
});
if (!response.success || !response.content) {
return {
success: false,
error: response.error || 'Email generation failed',
content: '',
};
}
const parsed = this.parseEmailContent(response.content);
const result = {
success: true,
content: parsed.body,
metadata: {
platform: 'email',
emailType,
subject: parsed.subject,
preheader: parsed.preheader,
generatedAt: new Date(),
model: this.provider.getProviderName(),
promptTokens: response.usage?.promptTokens || 0,
completionTokens: response.usage?.completionTokens || 0,
totalTokens: response.usage?.totalTokens || 0,
durationMs: Date.now() - startTime,
},
};
this.trackGeneration('email', emailType, result.metadata?.totalTokens || 0);
return result;
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Email generation failed',
content: '',
};
}
}
// ============================================
// SPECIALIZED GENERATION
// ============================================
/**
* Generate content from git commit data
*/
async generateFromCommit(commitData, outputFormats) {
// Validate commit data
if (!commitData.message || commitData.message.trim().length === 0) {
return {
success: false,
commit: commitData,
generatedContent: {},
totalGenerated: 0,
error: 'Commit message cannot be empty',
};
}
const results = {};
for (const format of outputFormats) {
switch (format) {
case 'blog':
results[format] = await this.generateBlogFromCommit(commitData);
break;
case 'social':
results[format] = await this.generateSocialFromCommit(commitData);
break;
case 'email':
results[format] = await this.generateEmailFromCommit(commitData);
break;
}
}
const successCount = Object.values(results).filter((r) => r?.success).length;
return {
success: successCount > 0,
commit: commitData,
generatedContent: results,
totalGenerated: successCount,
};
}
/**
* Generate a Twitter/LinkedIn thread from long-form content
*/
async generateThread(sourceContent, platform, maxPosts = 10) {
const startTime = Date.now();
// Validate source content
if (!sourceContent || sourceContent.trim().length === 0) {
return {
success: false,
posts: [],
error: 'Source content cannot be empty',
};
}
const prompt = this.buildThreadPrompt(sourceContent, platform, maxPosts);
try {
const response = await this.executeWithFallback({
messages: [
{
role: 'system',
content: `You are an expert at creating engaging ${platform} threads that break down complex topics.`,
},
{ role: 'user', content: prompt },
],
temperature: 0.7,
maxTokens: 3000,
});
if (!response.success || !response.content) {
return {
success: false,
posts: [],
error: response.error || 'Thread generation failed',
};
}
const posts = this.parseThreadContent(response.content, platform);
this.trackGeneration('thread', platform, response.usage?.totalTokens || 0);
return {
success: true,
posts,
totalPosts: posts.length,
platform,
metadata: {
generatedAt: new Date(),
durationMs: Date.now() - startTime,
},
};
}
catch (error) {
return {
success: false,
posts: [],
error: error instanceof Error ? error.message : 'Thread generation failed',
};
}
}
/**
* Generate hashtags for content
*/
async generateHashtags(content, count = 5, trending) {
// Validate content
if (!content || content.trim().length === 0) {
return {
success: false,
hashtags: [],
error: 'Content cannot be empty',
};
}
const prompt = `Generate ${count} relevant ${trending ? 'trending ' : ''}hashtags for this content. Return ONLY the hashtags, one per line, with the # symbol:\n\n${content}`;
try {
const response = await this.executeWithFallback({
messages: [
{
role: 'system',
content: 'You are a social media hashtag expert. Generate relevant, specific hashtags.',
},
{ role: 'user', content: prompt },
],
temperature: 0.5,
maxTokens: 200,
});
if (!response.success || !response.content) {
return {
success: false,
hashtags: [],
error: response.error || 'Hashtag generation failed',
};
}
const hashtags = this.parseHashtags(response.content);
return {
success: true,
hashtags,
count: hashtags.length,
};
}
catch (error) {
return {
success: false,
hashtags: [],
error: error instanceof Error ? error.message : 'Hashtag generation failed',
};
}
}
// ============================================
// BATCH OPERATIONS
// ============================================
/**
* Batch generate content from multiple requests
*/
async batchGenerate(requests) {
const results = await Promise.all(requests.map(async (req) => {
if (req.type === 'social' && req.platform) {
return {
id: req.id,
result: await this.generateSocialPost(req.sourceContent, req.platform, req.options),
};
}
else if (req.type === 'blog' && req.outline) {
return {
id: req.id,
result: await this.generateBlogPost(req.outline, req.blogOptions),
};
}
else if (req.type === 'email' && req.emailType) {
return {
id: req.id,
result: await this.generateEmail(req.sourceContent, req.emailType, req.emailOptions),
};
}
else {
return {
id: req.id,
result: {
success: false,
content: '',
error: 'Invalid request: missing required fields for the specified type',
},
};
}
}));
return {
success: true,
results,
totalGenerated: requests.length,
successCount: results.filter((r) => r.result.success).length,
failureCount: results.filter((r) => !r.result.success).length,
};
}
// ============================================
// ANALYTICS
// ============================================
/**
* Get generation analytics for a time range
*/
getAnalytics(timeRange) {
const filteredData = this.analyticsData.filter((d) => d.timestamp >= timeRange.start && d.timestamp <= timeRange.end);
const byPlatform = {};
const byType = {};
let totalTokens = 0;
for (const data of filteredData) {
byType[data.type] = (byType[data.type] || 0) + 1;
if (data.platform) {
byPlatform[data.platform] = (byPlatform[data.platform] || 0) + 1;
}
totalTokens += data.tokens;
}
// Group by date for trend data
const trendMap = new Map();
for (const data of filteredData) {
const dateKey = data.timestamp.toISOString().split('T')[0];
const existing = trendMap.get(dateKey) || { count: 0, tokens: 0 };
trendMap.set(dateKey, {
count: existing.count + 1,
tokens: existing.tokens + data.tokens,
});
}
const trendData = Array.from(trendMap.entries()).map(([dateStr, values]) => ({
date: new Date(dateStr),
count: values.count,
tokens: values.tokens,
}));
return {
totalGenerations: filteredData.length,
byPlatform,
byType,
averageTokens: filteredData.length > 0 ? totalTokens / filteredData.length : 0,
totalCost: totalTokens * 0.00002, // Rough cost estimate
trendData,
};
}
/**
* Clear analytics data
*/
clearAnalytics() {
this.analyticsData = [];
}
// ============================================
// PRIVATE METHODS
// ============================================
/**
* Execute a completion with fallback to secondary provider
*/
async executeWithFallback(request) {
try {
const response = await this.provider.complete(request);
if (response.success) {
return response;
}
// Try fallback if primary failed
if (this.fallbackProvider) {
return await this.fallbackProvider.complete(request);
}
return response;
}
catch (error) {
// Try fallback on error
if (this.fallbackProvider) {
try {
return await this.fallbackProvider.complete(request);
}
catch {
// Fallback also failed
}
}
throw error;
}
}
/**
* Build prompt for social media generation
*/
buildSocialPrompt(sourceContent, platform, options) {
const specs = this.getPlatformSpecifications(platform);
let prompt = `Create a ${platform} post from the following content.\n\n`;
prompt += `Platform Requirements:\n`;
prompt += `- Max length: ${specs.maxLength} characters\n`;
prompt += `- Tone: ${specs.tone}\n`;
prompt += `- Style: ${specs.style}\n\n`;
if (options?.includeHashtags && specs.supportsHashtags) {
prompt += `Include ${options.hashtagCount || 3} relevant hashtags\n`;
}
if (options?.includeEmojis) {
prompt += `Use appropriate emojis to enhance engagement\n`;
}
if (options?.callToAction) {
if (options.callToActionText) {
prompt += `Include this call to action: "${options.callToActionText}"\n`;
}
else {
prompt += `Include a compelling call to action\n`;
}
}
if (options?.tone) {
prompt += `Tone: ${options.tone}\n`;
}
if (options?.targetAudience) {
prompt += `Target audience: ${options.targetAudience}\n`;
}
prompt += `\nSource Content:\n${sourceContent}\n\n`;
prompt += `Provide ONLY the ${platform} post content without any additional explanation or meta-commentary.`;
return prompt;
}
/**
* Build prompt for blog generation
*/
buildBlogPrompt(outline, options) {
let prompt = `Write a comprehensive blog post based on this outline:\n\n`;
prompt += `Topic: ${outline.topic}\n`;
prompt += `Target Audience: ${outline.targetAudience}\n`;
prompt += `Tone: ${outline.tone}\n\n`;
prompt += `Sections:\n`;
outline.sections.forEach((section, i) => {
prompt += `\n${i + 1}. ${section.title}\n`;
section.keyPoints.forEach((point) => {
prompt += ` - ${point}\n`;
});
});
if (options?.includeIntro !== false) {
prompt += `\nInclude an engaging introduction that hooks the reader\n`;
}
if (options?.includeConclusion !== false) {
prompt += `Include a strong conclusion with key takeaways\n`;
}
if (options?.targetLength) {
prompt += `\nTarget length: approximately ${options.targetLength} words\n`;
}
if (options?.seoKeywords && options.seoKeywords.length > 0) {
prompt += `\nNaturally incorporate these SEO keywords: ${options.seoKeywords.join(', ')}\n`;
}
prompt += `\nProvide the blog post in markdown format.`;
return prompt;
}
/**
* Build prompt for email generation
*/
buildEmailPrompt(sourceContent, emailType, options) {
let prompt = `Create a ${emailType} email from the following content.\n\n`;
prompt += `Generate the email with these components:\n`;
prompt += `1. Subject line (compelling, ${options?.maxSubjectLength || 60} chars max)\n`;
prompt += `2. Preheader text (preview text, 100 chars max)\n`;
prompt += `3. Email body (well-structured HTML-friendly content)\n\n`;
if (options?.includeCallToAction !== false) {
prompt += `Include a clear call to action\n`;
}
if (options?.tone) {
prompt += `Tone: ${options.tone}\n`;
}
if (options?.targetAudience) {
prompt += `Target audience: ${options.targetAudience}\n`;
}
prompt += `\nSource Content:\n${sourceContent}\n\n`;
prompt += `Format your response as JSON: {"subject": "...", "preheader": "...", "body": "..."}`;
return prompt;
}
/**
* Build prompt for thread generation
*/
buildThreadPrompt(sourceContent, platform, maxPosts) {
const maxLength = platform === 'twitter' ? 280 : 3000;
let prompt = `Create a ${platform} thread (maximum ${maxPosts} posts) from this content.\n\n`;
prompt += `Requirements:\n`;
prompt += `- Each post must be under ${maxLength} characters\n`;
prompt += `- The first post should hook readers\n`;
prompt += `- Each post should flow naturally to the next\n`;
prompt += `- Use a consistent tone throughout\n`;
prompt += `- End with a summary or call to action\n\n`;
prompt += `Source Content:\n${sourceContent}\n\n`;
prompt += `Format as JSON array of strings: ["post1", "post2", ...]`;
return prompt;
}
/**
* Get platform specifications
*/
getPlatformSpecifications(platform) {
const specs = {
twitter: {
maxLength: 280,
tone: 'concise and engaging',
style: 'conversational with hashtags',
supportsHashtags: true,
supportsMedia: true,
maxHashtags: 3,
},
linkedin: {
maxLength: 3000,
tone: 'professional',
style: 'thought leadership',
supportsHashtags: true,
supportsMedia: true,
maxHashtags: 5,
},
facebook: {
maxLength: 63206,
tone: 'friendly and relatable',
style: 'conversational and visual',
supportsHashtags: true,
supportsMedia: true,
maxHashtags: 3,
},
instagram: {
maxLength: 2200,
tone: 'visual-first and aspirational',
style: 'storytelling with emojis',
supportsHashtags: true,
supportsMedia: true,
maxHashtags: 30,
},
mastodon: {
maxLength: 500,
tone: 'community-focused',
style: 'conversational',
supportsHashtags: true,
supportsMedia: true,
maxHashtags: 5,
},
bluesky: {
maxLength: 300,
tone: 'authentic',
style: 'conversational',
supportsHashtags: false,
supportsMedia: true,
},
};
return specs[platform];
}
/**
* Get max tokens for platform generation
*/
getPlatformMaxTokens(platform) {
const specs = this.getPlatformSpecifications(platform);
// Rough estimate: 1 token ≈ 4 characters
return Math.ceil(specs.maxLength / 3) + 100;
}
/**
* Parse and clean platform content
*/
parsePlatformContent(content, _platform) {
// Remove any leading/trailing whitespace and quotes
let cleaned = content.trim();
// Remove surrounding quotes if present
if ((cleaned.startsWith('"') && cleaned.endsWith('"')) ||
(cleaned.startsWith("'") && cleaned.endsWith("'"))) {
cleaned = cleaned.slice(1, -1);
}
return cleaned;
}
/**
* Parse email content from JSON response
*/
parseEmailContent(content) {
try {
// Try to extract JSON from the content
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
return {
subject: parsed.subject || 'Email Subject',
preheader: parsed.preheader || '',
body: parsed.body || content,
};
}
}
catch {
// JSON parsing failed
}
// Fallback: return content as body
return {
subject: 'Email Subject',
preheader: '',
body: content,
};
}
/**
* Parse thread content from JSON response
*/
parseThreadContent(content, _platform) {
try {
// Try to extract JSON array from the content
const jsonMatch = content.match(/\[[\s\S]*\]/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
if (Array.isArray(parsed)) {
return parsed.map((p) => String(p).trim());
}
}
}
catch {
// JSON parsing failed
}
// Fallback: split by common separators
const posts = content.split(/\n\n+/).filter((p) => p.trim().length > 0);
return posts.length > 0 ? posts : [content];
}
/**
* Parse hashtags from content
*/
parseHashtags(content) {
// Match hashtags with the # symbol
const hashtagMatches = content.match(/#\w+/g) || [];
// Remove # and return unique hashtags
const hashtags = hashtagMatches.map((tag) => tag.substring(1));
return [...new Set(hashtags)];
}
/**
* Generate alternate versions of content
*/
async generateAlternateVersions(sourceContent, platform, options, count) {
const versions = [];
for (let i = 0; i < count; i++) {
const result = await this.generateSocialPost(sourceContent, platform, {
...options,
alternateVersions: 0, // Prevent recursion
creativity: Math.min(1, (options.creativity || 0.7) + 0.1), // Slightly more creative
});
if (result.success && result.content) {
versions.push(result.content);
}
}
return versions;
}
/**
* Generate blog post from commit data
*/
async generateBlogFromCommit(commitData) {
const sourceContent = this.formatCommitForContent(commitData);
return this.generateBlogPost({
topic: `Development Update: ${commitData.message.split('\n')[0]}`,
sections: [
{
title: 'Overview',
keyPoints: [sourceContent],
},
{
title: 'Changes Made',
keyPoints: commitData.files.slice(0, 5),
},
],
targetAudience: 'developers and technical stakeholders',
tone: 'informative and technical',
}, {
targetLength: 500,
includeIntro: true,
includeConclusion: true,
});
}
/**
* Generate social post from commit data
*/
async generateSocialFromCommit(commitData) {
const sourceContent = this.formatCommitForContent(commitData);
return this.generateSocialPost(sourceContent, 'twitter', {
includeHashtags: true,
hashtagCount: 3,
includeEmojis: true,
tone: 'exciting and informative',
});
}
/**
* Generate email from commit data
*/
async generateEmailFromCommit(commitData) {
const sourceContent = this.formatCommitForContent(commitData);
return this.generateEmail(sourceContent, 'announcement', {
includeCallToAction: true,
tone: 'professional',
});
}
/**
* Format commit data for content generation
*/
formatCommitForContent(commitData) {
let content = `Commit: ${commitData.message}\n`;
content += `Author: ${commitData.author}\n`;
content += `Files changed: ${commitData.files.length}\n`;
content += `Changes: +${commitData.additions} -${commitData.deletions}\n`;
if (commitData.files.length > 0) {
content += `\nFiles:\n${commitData.files.slice(0, 10).join('\n')}`;
}
if (commitData.diff) {
content += `\n\nKey changes:\n${commitData.diff.substring(0, 1000)}`;
}
return content;
}
/**
* Count words in content
*/
countWords(content) {
return content.trim().split(/\s+/).filter((word) => word.length > 0).length;
}
/**
* Track generation for analytics
*/
trackGeneration(type, platform, tokens) {
if (this.config.enableAnalytics) {
this.analyticsData.push({
timestamp: new Date(),
type,
platform,
tokens,
});
}
}
}
//# sourceMappingURL=ai-content-generator.js.map