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:
2025-07-08 12:34:09 -06:00
parent 05acead999
commit 23f190c7a7

268
src/lib/ai.ts Normal file
View 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}`);
}