feat(feedback): Add content improvement feedback system

Frontend (frontend/app.js):

- Add textarea for improvement feedback

- Add submit button with loading state

- Handle API response and display improved content

Backend (backend/copywriter.py):

- Add improve_copy() method using Cohere API

- Integrate retry mechanism for API calls

Backend (backend/main.py):

- Add /improve-content POST endpoint

- Implement error handling and return improved content with metadata

Testing:

- Verified feedback submission flow

- Confirmed improved content generation

- Tested error scenarios and loading states
This commit is contained in:
Michael Ikehi
2025-04-18 19:19:10 +01:00
parent 12e0830ba6
commit 71ad7b4d26
8 changed files with 790 additions and 91 deletions
+665 -4
View File
@@ -1,3 +1,4 @@
// DOM Elements
document.addEventListener('DOMContentLoaded', function() {
// Navigation
@@ -23,6 +24,11 @@ document.addEventListener('DOMContentLoaded', function() {
const improvementFeedback = document.getElementById('improvement-feedback');
const submitImprovement = document.getElementById('submit-improvement');
// History Page
const historyFilterType = document.getElementById('history-filter-type');
const historySearch = document.getElementById('history-search');
const historyList = document.querySelector('.history-list');
// Brand Style Page
const toneSelector = document.getElementById('tone-selector');
const voiceSelector = document.getElementById('voice-selector');
@@ -45,6 +51,9 @@ document.addEventListener('DOMContentLoaded', function() {
const openRate = document.getElementById('open-rate');
const clickRate = document.getElementById('click-rate');
const conversionRate = document.getElementById('conversion-rate');
const trainingFilterType = document.getElementById('training-filter-type');
const trainingSearch = document.getElementById('training-search');
const trainingList = document.querySelector('.training-list');
// API Base URL
const API_URL = 'http://localhost:8000';
@@ -62,6 +71,16 @@ document.addEventListener('DOMContentLoaded', function() {
pages.forEach(page => {
if (page.id === `${pageName}-page`) {
page.classList.add('active');
// Load data for specific pages when they're opened
if (pageName === 'history') {
loadUserQueries();
} else if (pageName === 'training') {
// Check if the view training tab is active
if (document.querySelector('.tab[data-tab="view-training"]').classList.contains('active')) {
loadTrainingData();
}
}
} else {
page.classList.remove('active');
}
@@ -226,8 +245,278 @@ document.addEventListener('DOMContentLoaded', function() {
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
// Note: The backend automatically saves the query as part of the generate-copy endpoint
// so we don't need to make another API call here
});
}
// Load User Queries (History)
function loadUserQueries(page = 1, contentType = '') {
if (!historyList) return;
// Show loading state
historyList.innerHTML = '<div class="loading-state">Loading history...</div>';
// Build the query parameters
let queryParams = `?page=${page}&limit=10`;
// Call the API
fetch(`${API_URL}/user-queries${queryParams}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
historyList.innerHTML = '';
if (data.items.length === 0) {
historyList.innerHTML = '<div class="empty-state">No history found.</div>';
return;
}
// Filter by content type if provided
let filteredItems = data.items;
if (contentType) {
filteredItems = data.items.filter(item =>
item.parameters && item.parameters.content_type === contentType
);
}
// Create history items
filteredItems.forEach(item => {
const contentType = item.parameters?.content_type || 'general';
const timestamp = item.timestamp ? new Date(item.timestamp).toLocaleDateString() : 'Unknown date';
const promptPreview = item.prompt.length > 80 ? item.prompt.substring(0, 80) + '...' : item.prompt;
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
historyItem.innerHTML = `
<div class="history-item-type ${contentType || 'general'}">${getContentTypeLabel(contentType)}</div>
<div class="history-item-content">
<h4>${getPromptTitle(item.prompt)}</h4>
<p>${promptPreview}</p>
</div>
<div class="history-item-date">${timestamp}</div>
<div class="history-item-actions">
<button class="btn btn-icon view-query" title="View query" data-timestamp="${getTimestampFromISODate(item.timestamp)}">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-icon delete-query" title="Delete query" data-timestamp="${getTimestampFromISODate(item.timestamp)}">
<i class="fas fa-trash"></i>
</button>
</div>
`;
historyList.appendChild(historyItem);
});
// Add event listeners for view and delete buttons
document.querySelectorAll('.view-query').forEach(btn => {
btn.addEventListener('click', function() {
const timestamp = this.getAttribute('data-timestamp');
viewUserQuery(timestamp);
});
});
document.querySelectorAll('.delete-query').forEach(btn => {
btn.addEventListener('click', function() {
const timestamp = this.getAttribute('data-timestamp');
if (confirm('Are you sure you want to delete this query?')) {
deleteUserQuery(timestamp);
}
});
});
// Add pagination if needed
if (data.pagination && data.pagination.pages > 1) {
// Remove existing pagination if any
const existingPagination = document.querySelector('.pagination');
if (existingPagination) {
existingPagination.remove();
}
const paginationElement = document.createElement('div');
paginationElement.className = 'pagination';
let paginationHTML = '';
for (let i = 1; i <= data.pagination.pages; i++) {
paginationHTML += `<button class="page-btn ${i === page ? 'active' : ''}" data-page="${i}">${i}</button>`;
}
paginationElement.innerHTML = paginationHTML;
historyList.after(paginationElement);
// Add event listeners for pagination buttons
document.querySelectorAll('.page-btn').forEach(btn => {
btn.addEventListener('click', function() {
const pageNum = parseInt(this.getAttribute('data-page'));
loadUserQueries(pageNum, contentType);
});
});
}
})
.catch(error => {
console.error('Error loading user queries:', error);
historyList.innerHTML = '<div class="error-state">Error loading history. Please try again.</div>';
});
}
// Helper function to extract timestamp from ISO date
function getTimestampFromISODate(isoDate) {
if (!isoDate) return '';
const date = new Date(isoDate);
return date.toISOString().replace(/[-:T.]/g, '').slice(0, 14);
}
// Helper function to generate a title from prompt
function getPromptTitle(prompt) {
if (!prompt) return 'Untitled Query';
const words = prompt.split(' ');
if (words.length <= 5) return prompt;
return words.slice(0, 5).join(' ') + '...';
}
// Helper function to get display label for content type
function getContentTypeLabel(contentType) {
if (!contentType) return 'General';
const labels = {
'email': 'Email',
'social_media': 'Social',
'blog_post': 'Blog',
'website_copy': 'Website',
'sales_copy': 'Sales',
'ad_copy': 'Ad',
'video_script': 'Video',
'case_study': 'Case Study',
'product_description': 'Product',
'landing_page': 'Landing',
'press_release': 'Press',
'newsletter': 'Newsletter',
'general': 'General'
};
return labels[contentType] || contentType.charAt(0).toUpperCase() + contentType.slice(1);
}
// View User Query
function viewUserQuery(timestamp) {
fetch(`${API_URL}/user-queries/${timestamp}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
// Create a modal to display the query details
const modal = document.createElement('div');
modal.className = 'modal';
const modalContent = document.createElement('div');
modalContent.className = 'modal-content';
const parameters = data.parameters || {};
const contentType = parameters.content_type || 'Not specified';
const length = parameters.length || 'Not specified';
const includeCTA = parameters.include_cta ? 'Yes' : 'No';
modalContent.innerHTML = `
<div class="modal-header">
<h3>Query Details</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="detail-item">
<span class="detail-label">Prompt:</span>
<span class="detail-value">${data.prompt}</span>
</div>
<div class="detail-item">
<span class="detail-label">Content Type:</span>
<span class="detail-value">${contentType}</span>
</div>
<div class="detail-item">
<span class="detail-label">Length:</span>
<span class="detail-value">${length}</span>
</div>
<div class="detail-item">
<span class="detail-label">Include CTA:</span>
<span class="detail-value">${includeCTA}</span>
</div>
<div class="detail-item">
<span class="detail-label">Date:</span>
<span class="detail-value">${new Date(data.timestamp).toLocaleString()}</span>
</div>
</div>
`;
modal.appendChild(modalContent);
document.body.appendChild(modal);
// Close button functionality
modal.querySelector('.modal-close').addEventListener('click', function() {
document.body.removeChild(modal);
});
// Close when clicking outside the modal
window.addEventListener('click', function(event) {
if (event.target === modal) {
document.body.removeChild(modal);
}
});
})
.catch(error => {
console.error('Error viewing user query:', error);
alert('Error viewing query details. Please try again.');
});
}
// Delete User Query
function deleteUserQuery(timestamp) {
fetch(`${API_URL}/user-queries/${timestamp}`, {
method: 'DELETE'
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
alert('Query successfully deleted.');
// Reload the user queries
loadUserQueries();
})
.catch(error => {
console.error('Error deleting user query:', error);
alert('Error deleting query. Please try again.');
});
}
// History Filter Handlers
if (historyFilterType) {
historyFilterType.addEventListener('change', function() {
loadUserQueries(1, this.value);
});
}
if (historySearch) {
historySearch.addEventListener('input', function() {
// Client-side filtering - this would ideally be server-side,
// but we'll implement a simple client-side filter for now
const searchTerm = this.value.toLowerCase();
document.querySelectorAll('.history-item').forEach(item => {
const content = item.querySelector('.history-item-content').textContent.toLowerCase();
if (content.includes(searchTerm)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
}
});
});
}
@@ -408,6 +697,11 @@ document.addEventListener('DOMContentLoaded', function() {
tabContents.forEach(content => {
if (content.id === `${tabName}-tab`) {
content.classList.add('active');
// Load training data when the View tab is selected
if (tabName === 'view-training') {
loadTrainingData();
}
} else {
content.classList.remove('active');
}
@@ -479,8 +773,6 @@ document.addEventListener('DOMContentLoaded', function() {
// 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);
@@ -489,6 +781,257 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Load Training Data
function loadTrainingData(page = 1, contentType = '') {
if (!trainingList) return;
// Show loading state
trainingList.innerHTML = '<div class="loading-state">Loading training data...</div>';
// Build the query parameters
let queryParams = `?page=${page}&limit=10`;
if (contentType) {
queryParams += `&content_type=${contentType}`;
}
// Call the API
fetch(`${API_URL}/training-data${queryParams}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
trainingList.innerHTML = '';
if (data.items.length === 0) {
trainingList.innerHTML = '<div class="empty-state">No training data found.</div>';
return;
}
// Create training items
data.items.forEach(item => {
const trainingItem = document.createElement('div');
trainingItem.className = 'training-item';
// Generate metrics HTML
let metricsHTML = '';
if (item.metadata && item.metadata.performance_metrics) {
const metrics = item.metadata.performance_metrics;
if (metrics.open_rate) {
metricsHTML += `<span class="metric">Open Rate: ${(metrics.open_rate * 100).toFixed(1)}%</span>`;
}
if (metrics.click_rate) {
metricsHTML += `<span class="metric">Click Rate: ${(metrics.click_rate * 100).toFixed(1)}%</span>`;
}
if (metrics.conversion_rate) {
metricsHTML += `<span class="metric">Conversion: ${(metrics.conversion_rate * 100).toFixed(1)}%</span>`;
}
}
if (!metricsHTML) {
metricsHTML = '<span class="metric">No metrics available</span>';
}
const campaignName = item.metadata?.campaign_name || 'Untitled';
trainingItem.innerHTML = `
<div class="training-item-type ${item.content_type}">${getContentTypeLabel(item.content_type)}</div>
<div class="training-item-content">
<h4>${campaignName}</h4>
<p>Added on: ${new Date(item.added_at).toLocaleDateString()}</p>
<div class="metrics">
${metricsHTML}
</div>
</div>
<div class="training-item-actions">
<button class="btn btn-icon view-training" title="View content" data-id="${item.id}">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-icon delete-training" title="Delete content" data-id="${item.id}">
<i class="fas fa-trash"></i>
</button>
</div>
`;
trainingList.appendChild(trainingItem);
});
// Add event listeners for view and delete buttons
document.querySelectorAll('.view-training').forEach(btn => {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
viewTrainingData(id);
});
});
document.querySelectorAll('.delete-training').forEach(btn => {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
if (confirm('Are you sure you want to delete this training data?')) {
deleteTrainingData(id);
}
});
});
// Add pagination for training data
if (data.pagination && data.pagination.pages > 1) {
// Remove existing pagination if any
const existingPagination = document.querySelector('.pagination');
if (existingPagination) {
existingPagination.remove();
}
const paginationElement = document.createElement('div');
paginationElement.className = 'pagination';
let paginationHTML = '';
for (let i = 1; i <= data.pagination.pages; i++) {
paginationHTML += `<button class="page-btn ${i === page ? 'active' : ''}" data-page="${i}">${i}</button>`;
}
paginationElement.innerHTML = paginationHTML;
trainingList.after(paginationElement);
// Add event listeners for pagination buttons
document.querySelectorAll('.page-btn').forEach(btn => {
btn.addEventListener('click', function() {
const pageNum = parseInt(this.getAttribute('data-page'));
loadTrainingData(pageNum, contentType);
});
});
}
})
.catch(error => {
console.error('Error loading training data:', error);
trainingList.innerHTML = '<div class="error-state">Error loading training data. Please try again.</div>';
});
}
// View Training Data
function viewTrainingData(id) {
fetch(`${API_URL}/training-data/${id}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
// Create a modal to display the training data details
const modal = document.createElement('div');
modal.className = 'modal';
const modalContent = document.createElement('div');
modalContent.className = 'modal-content';
const campaignName = data.metadata?.campaign_name || 'Untitled';
let metricsHTML = '';
if (data.metadata && data.metadata.performance_metrics) {
const metrics = data.metadata.performance_metrics;
if (metrics.open_rate !== undefined) {
metricsHTML += `<div class="detail-item"><span class="detail-label">Open Rate:</span> <span class="detail-value">${(metrics.open_rate * 100).toFixed(1)}%</span></div>`;
}
if (metrics.click_rate !== undefined) {
metricsHTML += `<div class="detail-item"><span class="detail-label">Click Rate:</span> <span class="detail-value">${(metrics.click_rate * 100).toFixed(1)}%</span></div>`;
}
if (metrics.conversion_rate !== undefined) {
metricsHTML += `<div class="detail-item"><span class="detail-label">Conversion Rate:</span> <span class="detail-value">${(metrics.conversion_rate * 100).toFixed(1)}%</span></div>`;
}
}
modalContent.innerHTML = `
<div class="modal-header">
<h3>${campaignName}</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="detail-item">
<span class="detail-label">Content Type:</span>
<span class="detail-value">${getContentTypeLabel(data.content_type)}</span>
</div>
<div class="detail-item">
<span class="detail-label">Added On:</span>
<span class="detail-value">${new Date(data.added_at).toLocaleString()}</span>
</div>
${metricsHTML}
<div class="detail-item content-preview">
<span class="detail-label">Content:</span>
<div class="detail-value content-box">${data.content}</div>
</div>
</div>
`;
modal.appendChild(modalContent);
document.body.appendChild(modal);
// Close button functionality
modal.querySelector('.modal-close').addEventListener('click', function() {
document.body.removeChild(modal);
});
// Close when clicking outside the modal
window.addEventListener('click', function(event) {
if (event.target === modal) {
document.body.removeChild(modal);
}
});
})
.catch(error => {
console.error('Error viewing training data:', error);
alert('Error viewing training data details. Please try again.');
});
}
// Delete Training Data
function deleteTrainingData(id) {
fetch(`${API_URL}/training-data/${id}`, {
method: 'DELETE'
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
alert('Training data successfully deleted.');
// Reload the training data
loadTrainingData();
})
.catch(error => {
console.error('Error deleting training data:', error);
alert('Error deleting training data. Please try again.');
});
}
// Training Filter Handlers
if (trainingFilterType) {
trainingFilterType.addEventListener('change', function() {
loadTrainingData(1, this.value);
});
}
if (trainingSearch) {
trainingSearch.addEventListener('input', function() {
// Client-side filtering
const searchTerm = this.value.toLowerCase();
document.querySelectorAll('.training-item').forEach(item => {
const content = item.querySelector('.training-item-content').textContent.toLowerCase();
if (content.includes(searchTerm)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
}
});
});
}
// Load Brand Style on Page Load
fetch(`${API_URL}/brand-style`)
.then(response => {
@@ -508,4 +1051,122 @@ document.addEventListener('DOMContentLoaded', function() {
// 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 = 'Generate an email campaign for a product launch';
// Add CSS for modal
const modalStyle = document.createElement('style');
modalStyle.textContent = `
.modal {
display: block;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
overflow: auto;
}
.modal-content {
background-color: white;
margin: 5% auto;
padding: 0;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
width: 80%;
max-width: 800px;
animation: modalOpen 0.3s ease-out;
}
@keyframes modalOpen {
from {opacity: 0; transform: translateY(-20px);}
to {opacity: 1; transform: translateY(0);}
}
.modal-header {
padding: 20px 25px;
border-bottom: 1px solid var(--grey-200);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
}
.modal-close {
background: transparent;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--grey-600);
}
.modal-close:hover {
color: var(--grey-800);
}
.modal-body {
padding: 25px;
}
.detail-item {
margin-bottom: 15px;
}
.detail-label {
font-weight: 600;
color: var(--grey-700);
display: block;
margin-bottom: 5px;
}
.detail-value {
color: var(--grey-800);
}
.content-preview {
margin-top: 25px;
}
.content-box {
background-color: var(--grey-100);
border: 1px solid var(--grey-200);
border-radius: var(--radius-md);
padding: 15px;
margin-top: 10px;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
}
.loading-state, .empty-state, .error-state {
text-align: center;
padding: 30px;
color: var(--grey-500);
}
.pagination {
display: flex;
justify-content: center;
gap: 5px;
margin-top: 20px;
}
.page-btn {
padding: 8px 12px;
border: 1px solid var(--grey-300);
background-color: white;
border-radius: var(--radius-md);
cursor: pointer;
}
.page-btn.active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
`;
document.head.appendChild(modalStyle);
});