6fd7213076
- Set up FastAPI backend with modular structure: - main.py for API routing - copywriter.py for AI-powered content generation using Cohere - embeddings.py for generating and reranking content embeddings - vector_store.py for FAISS-based similarity search - brand_style.py for managing brand tone, taboo words, and preferred terms - config.py for managing environment and application settings - Configured RESTful API endpoints: /generate-copy, /brand-style, /training-data, /improve-content, /analyze-content - Created frontend with vanilla HTML, CSS, and JS (index.html, styles.css, app.js) - Integrated brand style management for tone, voice, taboo words, and terminology - Implemented vector search for referencing similar historical content - Enabled training data input to improve future AI output - Added environment variable support for API keys and model configs - Structured data storage with local JSON and DB files - Added developer documentation, API reference, and project setup instructions This commit provides the foundation for a full-stack, AI-driven content creation platform that ensures brand consistency, speeds up marketing workflows, and supports iterative improvement over time.
513 lines
20 KiB
JavaScript
513 lines
20 KiB
JavaScript
// DOM Elements
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Navigation
|
||
const menuItems = document.querySelectorAll('.menu li');
|
||
const pages = document.querySelectorAll('.page');
|
||
|
||
// Generate Content Page
|
||
const generateBtn = document.getElementById('generate-btn');
|
||
const promptInput = document.getElementById('prompt');
|
||
const contentTypeSelect = document.getElementById('content-type');
|
||
const toneSelect = document.getElementById('tone');
|
||
const lengthSelect = document.getElementById('length');
|
||
const includeCTACheckbox = document.getElementById('include-cta');
|
||
const referenceSimilarCheckbox = document.getElementById('reference-similar');
|
||
const resultContainer = document.getElementById('result-container');
|
||
const resultContent = document.getElementById('result-content');
|
||
const loadingIndicator = document.getElementById('loading-indicator');
|
||
const alignmentScore = document.getElementById('alignment-score');
|
||
const suggestionsList = document.getElementById('suggestions-list');
|
||
const copyBtn = document.getElementById('copy-btn');
|
||
const improveBtn = document.getElementById('improve-btn');
|
||
const saveBtn = document.getElementById('save-btn');
|
||
const improvementPanel = document.getElementById('improvement-panel');
|
||
const improvementFeedback = document.getElementById('improvement-feedback');
|
||
const submitImprovement = document.getElementById('submit-improvement');
|
||
|
||
// Brand Style Page
|
||
const toneSelector = document.getElementById('tone-selector');
|
||
const voiceSelector = document.getElementById('voice-selector');
|
||
const tabooWords = document.getElementById('taboo-words');
|
||
const tabooInput = document.getElementById('taboo-input');
|
||
const addTabooBtn = document.getElementById('add-taboo-btn');
|
||
const avoidTerm = document.getElementById('avoid-term');
|
||
const useTerm = document.getElementById('use-term');
|
||
const addTermBtn = document.getElementById('add-term-btn');
|
||
const saveBrandStyleBtn = document.getElementById('save-brand-style');
|
||
const resetBrandStyleBtn = document.getElementById('reset-brand-style');
|
||
|
||
// Training Page
|
||
const trainingTabs = document.querySelectorAll('.training-tabs .tab');
|
||
const tabContents = document.querySelectorAll('.tab-content');
|
||
const addTrainingBtn = document.getElementById('add-training-btn');
|
||
const trainingContentType = document.getElementById('training-content-type');
|
||
const campaignName = document.getElementById('campaign-name');
|
||
const trainingContent = document.getElementById('training-content');
|
||
const openRate = document.getElementById('open-rate');
|
||
const clickRate = document.getElementById('click-rate');
|
||
const conversionRate = document.getElementById('conversion-rate');
|
||
|
||
// API Base URL
|
||
const API_URL = 'http://localhost:8000';
|
||
|
||
// Menu Navigation
|
||
menuItems.forEach(item => {
|
||
item.addEventListener('click', function() {
|
||
const pageName = this.getAttribute('data-page');
|
||
|
||
// Update active menu item
|
||
menuItems.forEach(menuItem => menuItem.classList.remove('active'));
|
||
this.classList.add('active');
|
||
|
||
// Show selected page
|
||
pages.forEach(page => {
|
||
if (page.id === `${pageName}-page`) {
|
||
page.classList.add('active');
|
||
} else {
|
||
page.classList.remove('active');
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
// Generate Content
|
||
if (generateBtn) {
|
||
generateBtn.addEventListener('click', function() {
|
||
if (!promptInput.value.trim()) {
|
||
alert('Please enter a prompt for content generation.');
|
||
return;
|
||
}
|
||
|
||
// Show loading indicator
|
||
loadingIndicator.classList.remove('hidden');
|
||
resultContainer.classList.add('hidden');
|
||
|
||
// Prepare request data
|
||
const requestData = {
|
||
prompt: promptInput.value,
|
||
content_type: contentTypeSelect.value || null,
|
||
tone: toneSelect.value || null,
|
||
length: lengthSelect.value || null,
|
||
include_cta: includeCTACheckbox.checked,
|
||
reference_similar_content: referenceSimilarCheckbox.checked
|
||
};
|
||
|
||
// Call the API
|
||
fetch(`${API_URL}/generate-copy`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(requestData)
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
// Hide loading indicator
|
||
loadingIndicator.classList.add('hidden');
|
||
|
||
// Display result
|
||
resultContent.textContent = data.content;
|
||
|
||
// Set alignment score
|
||
const score = data.metadata.alignment_score || 0;
|
||
alignmentScore.style.width = `${score}%`;
|
||
alignmentScore.textContent = `${Math.round(score)}%`;
|
||
|
||
if (score < 60) {
|
||
alignmentScore.style.backgroundColor = 'var(--danger-color)';
|
||
} else if (score < 80) {
|
||
alignmentScore.style.backgroundColor = 'var(--warning-color)';
|
||
} else {
|
||
alignmentScore.style.backgroundColor = 'var(--success-color)';
|
||
}
|
||
|
||
// Display suggestions
|
||
if (data.suggestions && data.suggestions.length > 0) {
|
||
suggestionsList.innerHTML = '';
|
||
data.suggestions.forEach(suggestion => {
|
||
const li = document.createElement('li');
|
||
li.textContent = suggestion;
|
||
li.addEventListener('click', function() {
|
||
promptInput.value = suggestion;
|
||
});
|
||
suggestionsList.appendChild(li);
|
||
});
|
||
}
|
||
|
||
// Show result container
|
||
resultContainer.classList.remove('hidden');
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
loadingIndicator.classList.add('hidden');
|
||
alert('An error occurred while generating content. Please try again.');
|
||
});
|
||
});
|
||
}
|
||
|
||
// Copy to Clipboard
|
||
if (copyBtn) {
|
||
copyBtn.addEventListener('click', function() {
|
||
navigator.clipboard.writeText(resultContent.textContent)
|
||
.then(() => {
|
||
const originalTitle = copyBtn.getAttribute('title');
|
||
copyBtn.setAttribute('title', 'Copied!');
|
||
setTimeout(() => {
|
||
copyBtn.setAttribute('title', originalTitle);
|
||
}, 2000);
|
||
})
|
||
.catch(err => {
|
||
console.error('Could not copy text: ', err);
|
||
});
|
||
});
|
||
}
|
||
|
||
// Toggle Improvement Panel
|
||
if (improveBtn) {
|
||
improveBtn.addEventListener('click', function() {
|
||
improvementPanel.classList.toggle('hidden');
|
||
});
|
||
}
|
||
|
||
// Submit Improvement Feedback
|
||
if (submitImprovement) {
|
||
submitImprovement.addEventListener('click', function() {
|
||
if (!improvementFeedback.value.trim()) {
|
||
alert('Please enter feedback for improvement.');
|
||
return;
|
||
}
|
||
|
||
// Show loading indicator
|
||
loadingIndicator.classList.remove('hidden');
|
||
|
||
// Prepare request data
|
||
const requestData = {
|
||
content: resultContent.textContent,
|
||
feedback: improvementFeedback.value
|
||
};
|
||
|
||
// Call the API
|
||
fetch(`${API_URL}/improve-content`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(requestData)
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
// Hide loading indicator
|
||
loadingIndicator.classList.add('hidden');
|
||
|
||
// Update result content
|
||
resultContent.textContent = data.improved_content;
|
||
|
||
// Hide improvement panel
|
||
improvementPanel.classList.add('hidden');
|
||
improvementFeedback.value = '';
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
loadingIndicator.classList.add('hidden');
|
||
alert('An error occurred while improving content. Please try again.');
|
||
});
|
||
});
|
||
}
|
||
|
||
// Save Content to History
|
||
if (saveBtn) {
|
||
saveBtn.addEventListener('click', function() {
|
||
alert('Content saved to history!');
|
||
// In a real implementation, you would save this to local storage
|
||
// or call an API to save it to the backend
|
||
});
|
||
}
|
||
|
||
// Brand Style Tag Selection
|
||
if (toneSelector) {
|
||
const tagElements = toneSelector.querySelectorAll('.tag');
|
||
tagElements.forEach(tag => {
|
||
tag.addEventListener('click', function() {
|
||
this.classList.toggle('selected');
|
||
});
|
||
});
|
||
}
|
||
|
||
if (voiceSelector) {
|
||
const tagElements = voiceSelector.querySelectorAll('.tag');
|
||
tagElements.forEach(tag => {
|
||
tag.addEventListener('click', function() {
|
||
this.classList.toggle('selected');
|
||
});
|
||
});
|
||
}
|
||
|
||
// Add Taboo Word
|
||
if (addTabooBtn && tabooInput && tabooWords) {
|
||
addTabooBtn.addEventListener('click', function() {
|
||
const word = tabooInput.value.trim();
|
||
if (word) {
|
||
const tagElement = document.createElement('span');
|
||
tagElement.classList.add('tag', 'removable');
|
||
tagElement.innerHTML = `${word}<i class="fas fa-times"></i>`;
|
||
|
||
// Add click event to remove the tag
|
||
tagElement.querySelector('i').addEventListener('click', function() {
|
||
tagElement.remove();
|
||
});
|
||
|
||
tabooWords.appendChild(tagElement);
|
||
tabooInput.value = '';
|
||
}
|
||
});
|
||
|
||
// Allow pressing Enter to add a word
|
||
tabooInput.addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
addTabooBtn.click();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Add Terminology Term
|
||
if (addTermBtn && avoidTerm && useTerm) {
|
||
addTermBtn.addEventListener('click', function() {
|
||
const avoid = avoidTerm.value.trim();
|
||
const use = useTerm.value.trim();
|
||
|
||
if (avoid && use) {
|
||
const tableRow = document.createElement('div');
|
||
tableRow.classList.add('terminology-row');
|
||
tableRow.innerHTML = `
|
||
<div class="term-avoid">${avoid}</div>
|
||
<div class="term-use">${use}</div>
|
||
<div class="term-actions">
|
||
<button class="btn btn-icon"><i class="fas fa-times"></i></button>
|
||
</div>
|
||
`;
|
||
|
||
// Add click event to remove the row
|
||
tableRow.querySelector('.btn-icon').addEventListener('click', function() {
|
||
tableRow.remove();
|
||
});
|
||
|
||
// Insert before the add row
|
||
const addRow = document.querySelector('.terminology-row.add-row');
|
||
addRow.parentNode.insertBefore(tableRow, addRow);
|
||
|
||
avoidTerm.value = '';
|
||
useTerm.value = '';
|
||
}
|
||
});
|
||
|
||
// Allow pressing Enter to add a term
|
||
useTerm.addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
addTermBtn.click();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Save Brand Style
|
||
if (saveBrandStyleBtn) {
|
||
saveBrandStyleBtn.addEventListener('click', function() {
|
||
// Collect tone tags
|
||
const selectedTones = [];
|
||
toneSelector.querySelectorAll('.tag.selected').forEach(tag => {
|
||
selectedTones.push(tag.textContent);
|
||
});
|
||
|
||
// Collect voice characteristics
|
||
const selectedVoice = [];
|
||
voiceSelector.querySelectorAll('.tag.selected').forEach(tag => {
|
||
selectedVoice.push(tag.textContent);
|
||
});
|
||
|
||
// Collect taboo words
|
||
const tabooWordsList = [];
|
||
tabooWords.querySelectorAll('.tag').forEach(tag => {
|
||
// Extract just the text without the 'x' icon
|
||
const text = tag.textContent.replace('×', '').trim();
|
||
tabooWordsList.push(text);
|
||
});
|
||
|
||
// Collect preferred terms
|
||
const preferredTerms = {};
|
||
document.querySelectorAll('.terminology-row:not(.add-row):not(.terminology-header)').forEach(row => {
|
||
const avoid = row.querySelector('.term-avoid').textContent.trim();
|
||
const use = row.querySelector('.term-use').textContent.trim();
|
||
if (avoid && use) {
|
||
preferredTerms[avoid] = use;
|
||
}
|
||
});
|
||
|
||
// Prepare request data
|
||
const requestData = {
|
||
tone: selectedTones,
|
||
voice_characteristics: selectedVoice,
|
||
taboo_words: tabooWordsList,
|
||
preferred_terms: preferredTerms
|
||
};
|
||
|
||
// Call the API
|
||
fetch(`${API_URL}/brand-style`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(requestData)
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
alert('Brand style updated successfully!');
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('An error occurred while updating brand style. Please try again.');
|
||
});
|
||
});
|
||
}
|
||
|
||
// Reset Brand Style
|
||
if (resetBrandStyleBtn) {
|
||
resetBrandStyleBtn.addEventListener('click', function() {
|
||
if (confirm('Are you sure you want to reset brand style to defaults?')) {
|
||
// In a real implementation, you would call an API to reset
|
||
// For now, just reload the page
|
||
window.location.reload();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Training Tabs
|
||
if (trainingTabs.length > 0) {
|
||
trainingTabs.forEach(tab => {
|
||
tab.addEventListener('click', function() {
|
||
const tabName = this.getAttribute('data-tab');
|
||
|
||
// Update active tab
|
||
trainingTabs.forEach(t => t.classList.remove('active'));
|
||
this.classList.add('active');
|
||
|
||
// Show selected tab content
|
||
tabContents.forEach(content => {
|
||
if (content.id === `${tabName}-tab`) {
|
||
content.classList.add('active');
|
||
} else {
|
||
content.classList.remove('active');
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
// Add Training Data
|
||
if (addTrainingBtn) {
|
||
addTrainingBtn.addEventListener('click', function() {
|
||
if (!trainingContentType.value) {
|
||
alert('Please select a content type.');
|
||
return;
|
||
}
|
||
|
||
if (!trainingContent.value.trim()) {
|
||
alert('Please enter content for training.');
|
||
return;
|
||
}
|
||
|
||
// Prepare request data
|
||
const requestData = {
|
||
content_type: trainingContentType.value,
|
||
content: trainingContent.value,
|
||
metadata: {
|
||
campaign_name: campaignName.value,
|
||
performance_metrics: {}
|
||
}
|
||
};
|
||
|
||
// Add performance metrics if provided
|
||
if (openRate.value) {
|
||
requestData.metadata.performance_metrics.open_rate = parseFloat(openRate.value) / 100;
|
||
}
|
||
|
||
if (clickRate.value) {
|
||
requestData.metadata.performance_metrics.click_rate = parseFloat(clickRate.value) / 100;
|
||
}
|
||
|
||
if (conversionRate.value) {
|
||
requestData.metadata.performance_metrics.conversion_rate = parseFloat(conversionRate.value) / 100;
|
||
}
|
||
|
||
// Call the API
|
||
fetch(`${API_URL}/training-data`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(requestData)
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
alert('Training data added successfully!');
|
||
|
||
// Clear form
|
||
trainingContentType.value = '';
|
||
campaignName.value = '';
|
||
trainingContent.value = '';
|
||
openRate.value = '';
|
||
clickRate.value = '';
|
||
conversionRate.value = '';
|
||
|
||
// Switch to view tab
|
||
document.querySelector('.tab[data-tab="view-training"]').click();
|
||
|
||
// In a real implementation, you would also refresh the training data list
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('An error occurred while adding training data. Please try again.');
|
||
});
|
||
});
|
||
}
|
||
|
||
// Load Brand Style on Page Load
|
||
fetch(`${API_URL}/brand-style`)
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||
}
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
console.log('Loaded brand style:', data);
|
||
// In a real implementation, you would update the UI based on the loaded data
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading brand style:', error);
|
||
});
|
||
|
||
// For demonstration purposes, let's create a mocked pre-filled content
|
||
// In a real implementation, this would be loaded from the backend
|
||
document.getElementById('prompt').value = 'Write a social media post about our new coaching program';
|
||
}); |