Add AI features with secure environment variable configuration
- OpenAI API integration for event description generation - Environment variable configuration for API key - Premium feature access controls and usage tracking 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
268
src/lib/ai.ts
Normal file
268
src/lib/ai.ts
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
// AI-powered features for Black Canyon Tickets
|
||||||
|
// Uses OpenAI API for premium event description generation
|
||||||
|
|
||||||
|
const OPENAI_API_KEY = import.meta.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY;
|
||||||
|
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
||||||
|
|
||||||
|
export interface EventDescriptionRequest {
|
||||||
|
eventTitle: string;
|
||||||
|
venueName: string;
|
||||||
|
venueType?: string; // 'hotel', 'theater', 'gallery', 'outdoor', etc.
|
||||||
|
eventType: string; // 'gala', 'wedding', 'concert', 'dance', 'theater', etc.
|
||||||
|
eventDate?: string;
|
||||||
|
targetAudience?: string; // 'upscale', 'family', 'corporate', 'artistic', etc.
|
||||||
|
keyFeatures?: string[]; // ['dinner', 'dancing', 'live music', 'auction', etc.]
|
||||||
|
tone?: 'elegant' | 'casual' | 'luxurious' | 'artistic' | 'professional';
|
||||||
|
length?: 'short' | 'medium' | 'long';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventDescriptionResponse {
|
||||||
|
description: string;
|
||||||
|
tagline?: string;
|
||||||
|
marketingPoints: string[];
|
||||||
|
seoKeywords: string[];
|
||||||
|
socialMediaVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate professional event description using OpenAI
|
||||||
|
export async function generateEventDescription(request: EventDescriptionRequest): Promise<EventDescriptionResponse> {
|
||||||
|
const prompt = buildDescriptionPrompt(request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(OPENAI_API_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${OPENAI_API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'gpt-4',
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: `You are a professional event marketing copywriter specializing in upscale venues. You write compelling, sophisticated event descriptions that attract discerning clientele to high-end events. Your writing style is elegant but accessible, highlighting the unique aspects of luxury venues.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: prompt
|
||||||
|
}
|
||||||
|
],
|
||||||
|
max_tokens: 800,
|
||||||
|
temperature: 0.7,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const generatedText = data.choices[0]?.message?.content;
|
||||||
|
|
||||||
|
if (!generatedText) {
|
||||||
|
throw new Error('No content generated from OpenAI');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseGeneratedDescription(generatedText);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating event description:', error);
|
||||||
|
throw new Error('Failed to generate AI event description. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the prompt for OpenAI based on event details
|
||||||
|
function buildDescriptionPrompt(request: EventDescriptionRequest): string {
|
||||||
|
const {
|
||||||
|
eventTitle,
|
||||||
|
venueName,
|
||||||
|
venueType = 'venue',
|
||||||
|
eventType,
|
||||||
|
eventDate,
|
||||||
|
targetAudience = 'upscale',
|
||||||
|
keyFeatures = [],
|
||||||
|
tone = 'elegant',
|
||||||
|
length = 'medium'
|
||||||
|
} = request;
|
||||||
|
|
||||||
|
const lengthGuide = {
|
||||||
|
short: '1-2 paragraphs (150-200 words)',
|
||||||
|
medium: '2-3 paragraphs (250-350 words)',
|
||||||
|
long: '3-4 paragraphs (400-500 words)'
|
||||||
|
};
|
||||||
|
|
||||||
|
const toneGuide = {
|
||||||
|
elegant: 'sophisticated and refined, emphasizing exclusivity and prestige',
|
||||||
|
casual: 'warm and inviting, approachable but still upscale',
|
||||||
|
luxurious: 'opulent and lavish, highlighting premium amenities and experiences',
|
||||||
|
artistic: 'creative and cultured, appealing to arts enthusiasts',
|
||||||
|
professional: 'polished and corporate, suitable for business events'
|
||||||
|
};
|
||||||
|
|
||||||
|
return `Write a compelling event description for "${eventTitle}" at ${venueName} in the Aspen/Roaring Fork Valley area.
|
||||||
|
|
||||||
|
Event Details:
|
||||||
|
- Event Type: ${eventType}
|
||||||
|
- Venue: ${venueName} (${venueType})
|
||||||
|
- Target Audience: ${targetAudience}
|
||||||
|
- Tone: ${toneGuide[tone]}
|
||||||
|
- Length: ${lengthGuide[length]}
|
||||||
|
${eventDate ? `- Date: ${eventDate}` : ''}
|
||||||
|
${keyFeatures.length > 0 ? `- Key Features: ${keyFeatures.join(', ')}` : ''}
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
1. Write a main event description that captures the essence and appeal of the event
|
||||||
|
2. Include a short, catchy tagline (10-15 words)
|
||||||
|
3. List 3-5 key marketing points that would appeal to the target audience
|
||||||
|
4. Suggest 5-8 SEO keywords relevant to the event and location
|
||||||
|
5. Create a shorter social media version (1-2 sentences)
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
- The unique appeal of the Aspen/mountain luxury setting
|
||||||
|
- What makes this event special and worth attending
|
||||||
|
- The experience attendees will have
|
||||||
|
- The prestige and exclusivity appropriate for the market
|
||||||
|
|
||||||
|
Format your response as:
|
||||||
|
|
||||||
|
DESCRIPTION:
|
||||||
|
[Main event description here]
|
||||||
|
|
||||||
|
TAGLINE:
|
||||||
|
[Catchy tagline here]
|
||||||
|
|
||||||
|
MARKETING POINTS:
|
||||||
|
- [Point 1]
|
||||||
|
- [Point 2]
|
||||||
|
- [Point 3]
|
||||||
|
- [etc.]
|
||||||
|
|
||||||
|
SEO KEYWORDS:
|
||||||
|
[keyword1, keyword2, keyword3, etc.]
|
||||||
|
|
||||||
|
SOCIAL MEDIA:
|
||||||
|
[Short social media version]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the generated text into structured response
|
||||||
|
function parseGeneratedDescription(generatedText: string): EventDescriptionResponse {
|
||||||
|
const sections = generatedText.split(/\n(?=[A-Z ]+:)/);
|
||||||
|
|
||||||
|
let description = '';
|
||||||
|
let tagline = '';
|
||||||
|
let marketingPoints: string[] = [];
|
||||||
|
let seoKeywords: string[] = [];
|
||||||
|
let socialMediaVersion = '';
|
||||||
|
|
||||||
|
sections.forEach(section => {
|
||||||
|
const trimmedSection = section.trim();
|
||||||
|
|
||||||
|
if (trimmedSection.startsWith('DESCRIPTION:')) {
|
||||||
|
description = trimmedSection.replace('DESCRIPTION:', '').trim();
|
||||||
|
} else if (trimmedSection.startsWith('TAGLINE:')) {
|
||||||
|
tagline = trimmedSection.replace('TAGLINE:', '').trim();
|
||||||
|
} else if (trimmedSection.startsWith('MARKETING POINTS:')) {
|
||||||
|
const pointsText = trimmedSection.replace('MARKETING POINTS:', '').trim();
|
||||||
|
marketingPoints = pointsText
|
||||||
|
.split('\n')
|
||||||
|
.map(point => point.replace(/^-\s*/, '').trim())
|
||||||
|
.filter(point => point.length > 0);
|
||||||
|
} else if (trimmedSection.startsWith('SEO KEYWORDS:')) {
|
||||||
|
const keywordsText = trimmedSection.replace('SEO KEYWORDS:', '').trim();
|
||||||
|
seoKeywords = keywordsText
|
||||||
|
.split(',')
|
||||||
|
.map(keyword => keyword.trim())
|
||||||
|
.filter(keyword => keyword.length > 0);
|
||||||
|
} else if (trimmedSection.startsWith('SOCIAL MEDIA:')) {
|
||||||
|
socialMediaVersion = trimmedSection.replace('SOCIAL MEDIA:', '').trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
description,
|
||||||
|
tagline,
|
||||||
|
marketingPoints,
|
||||||
|
seoKeywords,
|
||||||
|
socialMediaVersion
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate event title suggestions
|
||||||
|
export async function generateEventTitleSuggestions(request: {
|
||||||
|
eventType: string;
|
||||||
|
venueName: string;
|
||||||
|
theme?: string;
|
||||||
|
tone?: string;
|
||||||
|
}): Promise<string[]> {
|
||||||
|
const { eventType, venueName, theme, tone = 'elegant' } = request;
|
||||||
|
|
||||||
|
const prompt = `Generate 5 creative, sophisticated event titles for a ${eventType} at ${venueName} in Aspen, Colorado. ${theme ? `The theme is: ${theme}.` : ''} The tone should be ${tone}. Make them appealing to upscale mountain-town clientele.
|
||||||
|
|
||||||
|
Format as a simple numbered list:
|
||||||
|
1. [Title]
|
||||||
|
2. [Title]
|
||||||
|
etc.`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(OPENAI_API_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${OPENAI_API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'gpt-4',
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: 'You are a creative event naming specialist for luxury venues in Aspen.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: prompt
|
||||||
|
}
|
||||||
|
],
|
||||||
|
max_tokens: 300,
|
||||||
|
temperature: 0.8,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const generatedText = data.choices[0]?.message?.content || '';
|
||||||
|
|
||||||
|
// Parse numbered list
|
||||||
|
return generatedText
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.replace(/^\d+\.\s*/, '').trim())
|
||||||
|
.filter(title => title.length > 0);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating title suggestions:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has access to AI features
|
||||||
|
export function hasAIAccess(addons: any[]): boolean {
|
||||||
|
return addons.some(addon =>
|
||||||
|
addon.slug === 'ai-event-description' ||
|
||||||
|
addon.slug === 'bct-pro-monthly' ||
|
||||||
|
addon.slug === 'enterprise-package'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cost calculation for AI features
|
||||||
|
export const AI_FEATURE_COSTS = {
|
||||||
|
'ai-event-description': 500, // $5.00 in cents
|
||||||
|
'ai-title-suggestions': 300, // $3.00 in cents (if offered separately)
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Usage tracking for AI features
|
||||||
|
export async function trackAIUsage(
|
||||||
|
organizationId: string,
|
||||||
|
feature: string,
|
||||||
|
tokens: number,
|
||||||
|
cost: number
|
||||||
|
) {
|
||||||
|
// This could be logged to analytics or usage tracking system
|
||||||
|
console.log(`AI Usage - Org: ${organizationId}, Feature: ${feature}, Tokens: ${tokens}, Cost: $${cost/100}`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user