From 71ad7b4d26e49b4f2be8b0d4cab29bfcd7b0555d Mon Sep 17 00:00:00 2001 From: Michael Ikehi Date: Fri, 18 Apr 2025 19:19:10 +0100 Subject: [PATCH] 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 --- .gitignore | 6 +- backend/main.py | 32 +- data/training_data.db | Bin 8192 -> 8192 bytes data/vector_store/faiss_index.bin | Bin 41005 -> 12333 bytes data/vector_store/metadata.pkl | Bin 15172 -> 3917 bytes frontend/app.js | 669 +++++++++++++++++++++++++++++- frontend/index.html | 88 +--- logs/app.log | 86 ++++ 8 files changed, 790 insertions(+), 91 deletions(-) diff --git a/.gitignore b/.gitignore index 5d1220a..a9a9a5c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,8 +36,10 @@ ENV/ # Local data data/user_queries/* !data/user_queries/.gitkeep -backend/data/vector_store/* -!backend/data/vector_store/.gitkeep +data/past_campaigns/* +!data/past_campaigns/.gitkeep +data/vector_store/* +data/training_data/* # Logs logs/* diff --git a/backend/main.py b/backend/main.py index 4aace71..3c76b7c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -382,28 +382,36 @@ async def list_user_queries( page: int = Query(1, ge=1, description="Page number"), limit: int = Query(10, ge=1, le=100, description="Items per page") ): - """Retrieve a list of user queries.""" + """List user queries with pagination.""" try: - # Get all query files - query_files = glob.glob(str(Path(config.DATA_DIR) / "user_queries" / "*.json")) - query_files.sort(reverse=True) # Sort by filename (timestamp) descending + # Calculate offset + offset = (page - 1) * limit + + # Get files from user_queries directory + query_dir = Path(config.DATA_DIR) / "user_queries" + query_dir.mkdir(exist_ok=True) + + # List all JSON files and sort by name (timestamp) in descending order + files = sorted(query_dir.glob("*.json"), reverse=True) + total = len(files) # Apply pagination - start_idx = (page - 1) * limit - end_idx = start_idx + limit - page_files = query_files[start_idx:end_idx] + files = files[offset:offset + limit] items = [] - for file_path in page_files: - with open(file_path, 'r') as f: + for file in files: + with open(file, 'r') as f: query_data = json.load(f) items.append(query_data) return { "items": items, - "total": len(query_files), - "page": page, - "limit": limit + "pagination": { + "total": total, + "page": page, + "limit": limit, + "pages": (total + limit - 1) // limit + } } except Exception as e: logger.error(f"Error listing user queries: {str(e)}") diff --git a/data/training_data.db b/data/training_data.db index 1691c6d0a9bade890e0fd57c7ac80d0f8eae0114..189ca4e8c3289db1c3df2e6ee8cad84bc845bc32 100644 GIT binary patch literal 8192 zcmeI1(N7yk5XNm(sl`&OzU6h`g`~1AAfcp4RceS;XbGfXicnOc1g8rCyR4=lh6NktTejQcb{Wc^NT+lc-2Qupe9fgs0q{rY63NZnm|pUCQuWo z3DgAsKLT$*YkqofW25=|X;e3cqgUE$JG#zQR7)RL4$k_$L7xV_{o_6@4blD9s?O=? zWYGVe`!>Q>*e_NPfw5gz2zmIpB+8xon6t7{VTe!^9QX5X>Z>8s)1L1)C6h* zHG!HyO`s-F6Q~K)1U?|}?!39$_~YTdx8L?w57`(;FHY2ie`&W`J?)>COR-Fi&-|um7I;n$P;R{F4<&m!W11Wm@-q|v}sb&&}+8& z8fNU5k!G@(9@0!Enmjp84q)SXBakA?vxRSRHmx;L{+8oBkeN1yvazxw#DP;cOxK@K zk33JEDWi5)`HFI$>I`1QVKTy(Ya8TeGI8oDSK3tM9SrO|r5m0nfas$yLj((4D3ryD zVzV(7morrbnkj4$@-7`kgd7a>ZJP==V?QiSJMqyrmO27Xm(Ep1wEh4hfJ0q?G0ew(W8%DN4kE{@JvoNRXfXO+OYlyOe>r{nE zU;uD{mO0}*aHEukijlnyZ3-@#X3A25r2*3D-;y#sMa~eSBmByUlZ3Fe`en%!Vuc9O zN_R%F>XMXON6H-syAd(V6_TRoH5&XQQz6ZP_+_b!0^pMZ12%Kqal;PGI?6}LQ^nF_ zUaIOu-6FZtW~t;xM~JPoUAi2z_}9#^by^p#49p-$nB5d zr?Hx|%tZcDZ;|<2`iKqa?&6Tuiqb=u0ns^YL{ozC2BlG@6?a{vtt6z`xDx%l;SfP8 z@;cmhhMq-rS{Wg)9enj)$r;-uVy0hRC3Op|CWb>0Ay`K0Q^|YYk%jUQ02}awY5GN2 ztP@KR`J1@b=n5S_R#3HdaV#mIyhaow%ONl{x9qN^Hj6I6gb}vd0^;1Y(d0V2g|?wk zF=gc~O`Pae*hqO)K1Hjjx#pzp_!aI90evygAEk7t3GO9xNiFLigSo1tK}0dB!DN0a zJfkG&a`YCKxfSLC5f2S(|M2=ZTWba{e1!t3=+AzAd&wZ*-0 zeUH|Qv(u*+2Lm#_i<5)H^){}&hkK3PzSmj6#Mf^yw0Lrmo(_m{T2yTA_Wp8ldvklI tv-zmA^?0!LcyDKSZ)>OfaQmCDcOS{jrH25Rj(ZWmd2d1=n(Xdu{sj_SW6uBp literal 8192 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 diff --git a/data/vector_store/metadata.pkl b/data/vector_store/metadata.pkl index 2ca050301cc168caf2c973d3dea0269cfe624fe1..78ae91a2a6351e311d7c1317d64964888a89022b 100644 GIT binary patch literal 3917 zcmeHKO>Y!O5OoNUuz}>jEteGHU=Hhzu>mi>SYQ%^KoG@72BFC6ovGPr+|xaD_v|uA zNI7!YGq%5V6l&Pv#dTOa~o)lqa-ei%CYHU_^iwo1`j=X!f zxDa*4A!0|j_<3V}jkW9JgX`n%o42+%*GIR<zc}~Bpne!ST?5TXpahvYxkhvNAboPrsmJmEv75_CD+-KOO z7wOk+>nmidz1^_@09$5s_+a_nzS(Wm~9D7IR+8D}8WvB21 zkFH4Xcjyjzo;lM*?X2=Ge^fn}j~Fm6gsRT?NvHBABTV z2^au4fR;Jqyl^$ATE)oTh9L!)wq~fK8bJ^byyl8EY+F+v)xs*G z(w3a8)G>lXMW>V-9YMCnj_9ytu><2sPL8DJQYfKj&I6$zOn?fKKBY+tyd|RxR4?{? z20xXm$1>HCb5^PtO~}#&sX@~bb0dVU#HD%J3jbYE zz)3}3hvT)vv#?GpBgkt9Tfe=QC$@>iOfOw4c?+y6!yyO}ELwt|N!)!!*2;qbY=9rO z3e}bDPFQ{!-4*E_2obEdE|wAk(rbhLX87B88a}3Gwx!PVDnV@90fIa0gOHErZ&=2YPVe@L+#T05p`tI zk-No@^`B4fJ>5MZbLZ*f-TU3*@<%B=suj3pbB)6;=1H{O;@P$K&y>@m5z}czC89JiB~uW#x2AoKA_;DRDX_ zPNxL4cWU0F)ViTXf#tGb<~PoJ*oY21^iGEvoox;ar5O$lDz{WRas&7iI2XLSq^-RQ3~0 z8w1q@%TRbm1OmjX=myJ}#~w2Y@+ab-jOa8&`_YeSIpH{GOhC*~k!dn%r!uI;fa+D# ziB=Uk32_4HA!myKP6vt<)S+C;QCPv=RU8$d+T^@KC((pi4t*jQnnK49f^E6jd=xwE zoM>F!=zN;A$!(eZb106XNIHS2k3&Jt#R$=obVyQ=a%B_aGP~t=12foxoIuM|5PnG~ z$|W!@D&0JTitATR1V}+$?9MUM1%N%=MNZ^_QBYVo^hEJ2?<{lYalJ`e14^yHMNXuc ztb&@tVJU14CEcs%bVCuKd(FO+Byul!kdF6KCO8B2JQ!Tvhxhl?n>UCvJwHPJ@q_@M O*3S{%o@k1ONB;uMplXx= literal 15172 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 TD9Pis@b-$^kea|n+~|J*$9|7M diff --git a/frontend/app.js b/frontend/app.js index 17e6711..32444ca 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -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 = '
Loading history...
'; + + // 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 = '
No history found.
'; + 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 = ` +
${getContentTypeLabel(contentType)}
+
+

${getPromptTitle(item.prompt)}

+

${promptPreview}

+
+
${timestamp}
+
+ + +
+ `; + + 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 += ``; + } + + 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 = '
Error loading history. Please try again.
'; + }); + } + + // 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 = ` + + + `; + + 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 = '
Loading training data...
'; + + // 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 = '
No training data found.
'; + 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 += `Open Rate: ${(metrics.open_rate * 100).toFixed(1)}%`; + } + if (metrics.click_rate) { + metricsHTML += `Click Rate: ${(metrics.click_rate * 100).toFixed(1)}%`; + } + if (metrics.conversion_rate) { + metricsHTML += `Conversion: ${(metrics.conversion_rate * 100).toFixed(1)}%`; + } + } + + if (!metricsHTML) { + metricsHTML = 'No metrics available'; + } + + const campaignName = item.metadata?.campaign_name || 'Untitled'; + + trainingItem.innerHTML = ` +
${getContentTypeLabel(item.content_type)}
+
+

${campaignName}

+

Added on: ${new Date(item.added_at).toLocaleDateString()}

+
+ ${metricsHTML} +
+
+ +
+ + +
+ `; + + 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 += ``; + } + + 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 = '
Error loading training data. Please try again.
'; + }); + } + + // 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 += `
Open Rate: ${(metrics.open_rate * 100).toFixed(1)}%
`; + } + if (metrics.click_rate !== undefined) { + metricsHTML += `
Click Rate: ${(metrics.click_rate * 100).toFixed(1)}%
`; + } + if (metrics.conversion_rate !== undefined) { + metricsHTML += `
Conversion Rate: ${(metrics.conversion_rate * 100).toFixed(1)}%
`; + } + } + + modalContent.innerHTML = ` + + + `; + + 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); }); diff --git a/frontend/index.html b/frontend/index.html index 76be6f3..d349df1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,3 +1,4 @@ + @@ -198,9 +199,18 @@
@@ -209,45 +219,7 @@
-
- -
-

Transformation Masterclass Invitation

-

Subject: Transform Your Potential with Adriana James' Exclusive Workshop...

-
-
Apr 17, 2025
-
- - - -
-
-
- -
-

3-Step Framework Post

-

BREAKTHROUGH MOMENT ✨ Ever feel stuck in patterns that hold you back...

-
-
Apr 16, 2025
-
- - - -
-
-
-
Blog
-
-

5 Ways to Overcome Limiting Beliefs

-

Are limiting beliefs holding you back from achieving your full potential?...

-
-
Apr 14, 2025
-
- - - -
-
+
@@ -391,7 +363,7 @@ - + +
@@ -446,38 +419,7 @@
-
- -
-

Transformation Masterclass Promotion

-

Added on: Apr 15, 2025

-
- Open Rate: 42% - Click Rate: 18% - Conversion: 8% -
-
-
- - -
-
-
- -
-

Breakthrough Framework

-

Added on: Apr 10, 2025

-
- Engagement: 6.4% - Saves: 178 - Shares: 92 -
-
-
- - -
-
+
diff --git a/logs/app.log b/logs/app.log index 2412080..95a31a6 100644 --- a/logs/app.log +++ b/logs/app.log @@ -882,3 +882,89 @@ 2025-04-18 17:41:55.046 | INFO | copywriter:generate_copy:90 - Generated content with 2070 characters 2025-04-18 17:41:55.458 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store 2025-04-18 17:41:55.458 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:11:18.236 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 18:11:18.236 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 18:11:40.648 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 18:11:40.648 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 18:13:01.347 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 18:13:01.347 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 18:36:18.449 | ERROR | main:delete_user_query:461 - Error deleting user query: 404: Query with timestamp 20250418164155 not found +2025-04-18 18:36:18.449 | ERROR | main:delete_user_query:461 - Error deleting user query: 404: Query with timestamp 20250418164155 not found +2025-04-18 18:36:43.730 | ERROR | main:delete_user_query:461 - Error deleting user query: 404: Query with timestamp 20250418161237 not found +2025-04-18 18:36:43.730 | ERROR | main:delete_user_query:461 - Error deleting user query: 404: Query with timestamp 20250418161237 not found +2025-04-18 18:37:32.259 | ERROR | copywriter:_call_llm_api:139 - Error calling Cohere API: +2025-04-18 18:37:32.259 | ERROR | copywriter:_call_llm_api:139 - Error calling Cohere API: +2025-04-18 18:38:06.509 | ERROR | copywriter:_call_llm_api:139 - Error calling Cohere API: +2025-04-18 18:38:06.509 | ERROR | copywriter:_call_llm_api:139 - Error calling Cohere API: +2025-04-18 18:38:48.267 | INFO | copywriter:_generate_headline_suggestions:188 - Generated 3 headline suggestions +2025-04-18 18:38:48.267 | INFO | copywriter:_generate_headline_suggestions:188 - Generated 3 headline suggestions +2025-04-18 18:38:48.272 | INFO | copywriter:generate_copy:90 - Generated content with 3812 characters +2025-04-18 18:38:48.272 | INFO | copywriter:generate_copy:90 - Generated content with 3812 characters +2025-04-18 18:38:51.235 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:38:51.235 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:39:59.364 | ERROR | main:get_user_query:437 - Error getting user query: 404: Query with timestamp 20250418173851 not found +2025-04-18 18:39:59.364 | ERROR | main:get_user_query:437 - Error getting user query: 404: Query with timestamp 20250418173851 not found +2025-04-18 18:40:23.978 | ERROR | main:get_user_query:437 - Error getting user query: 404: Query with timestamp 20250418173851 not found +2025-04-18 18:40:23.978 | ERROR | main:get_user_query:437 - Error getting user query: 404: Query with timestamp 20250418173851 not found +2025-04-18 18:43:01.011 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:43:01.011 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:43:07.295 | ERROR | main:get_training_data:306 - Error retrieving training data: Column expression, FROM clause, or other columns clause element expected, got [Table('training_data', MetaData(), Column('id', Integer(), table=, primary_key=True, nullable=False), Column('content', String(), table=, nullable=False), Column('content_type', String(), table=, nullable=False), Column('metadata', JSON(), table=, nullable=False), Column('added_at', DateTime(), table=, nullable=False, default=CallableColumnDefault()), Column('is_training_data', Boolean(), table=, nullable=False, default=ScalarElementColumnDefault(True)), schema=None)]. Did you mean to say select(Table('training_data', MetaData(), Column('id', Integer(), table=, primary_key=True, nullable=False), Column('content', String(), table=, nullable=False), Column('content_type', String(), table=, nullable=False), Column('metadata', JSON(), table=, nullable=False), Column('added_at', DateTime(), table=, nullable=False, default=CallableColumnDefault()), Column('is_training_data', Boolean(), table=, nullable=False, default=ScalarElementColumnDefault(True)), schema=None))? +2025-04-18 18:43:07.295 | ERROR | main:get_training_data:306 - Error retrieving training data: Column expression, FROM clause, or other columns clause element expected, got [Table('training_data', MetaData(), Column('id', Integer(), table=, primary_key=True, nullable=False), Column('content', String(), table=, nullable=False), Column('content_type', String(), table=, nullable=False), Column('metadata', JSON(), table=, nullable=False), Column('added_at', DateTime(), table=, nullable=False, default=CallableColumnDefault()), Column('is_training_data', Boolean(), table=, nullable=False, default=ScalarElementColumnDefault(True)), schema=None)]. Did you mean to say select(Table('training_data', MetaData(), Column('id', Integer(), table=, primary_key=True, nullable=False), Column('content', String(), table=, nullable=False), Column('content_type', String(), table=, nullable=False), Column('metadata', JSON(), table=, nullable=False), Column('added_at', DateTime(), table=, nullable=False, default=CallableColumnDefault()), Column('is_training_data', Boolean(), table=, nullable=False, default=ScalarElementColumnDefault(True)), schema=None))? +2025-04-18 18:44:21.955 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:44:21.955 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:44:28.293 | INFO | vector_store:delete_document:256 - Marked document 2 as deleted +2025-04-18 18:44:28.293 | INFO | vector_store:delete_document:256 - Marked document 2 as deleted +2025-04-18 18:44:32.713 | ERROR | main:get_training_data:306 - Error retrieving training data: Column expression, FROM clause, or other columns clause element expected, got [Table('training_data', MetaData(), Column('id', Integer(), table=, primary_key=True, nullable=False), Column('content', String(), table=, nullable=False), Column('content_type', String(), table=, nullable=False), Column('metadata', JSON(), table=, nullable=False), Column('added_at', DateTime(), table=, nullable=False, default=CallableColumnDefault()), Column('is_training_data', Boolean(), table=, nullable=False, default=ScalarElementColumnDefault(True)), schema=None)]. Did you mean to say select(Table('training_data', MetaData(), Column('id', Integer(), table=, primary_key=True, nullable=False), Column('content', String(), table=, nullable=False), Column('content_type', String(), table=, nullable=False), Column('metadata', JSON(), table=, nullable=False), Column('added_at', DateTime(), table=, nullable=False, default=CallableColumnDefault()), Column('is_training_data', Boolean(), table=, nullable=False, default=ScalarElementColumnDefault(True)), schema=None))? +2025-04-18 18:44:32.713 | ERROR | main:get_training_data:306 - Error retrieving training data: Column expression, FROM clause, or other columns clause element expected, got [Table('training_data', MetaData(), Column('id', Integer(), table=, primary_key=True, nullable=False), Column('content', String(), table=, nullable=False), Column('content_type', String(), table=, nullable=False), Column('metadata', JSON(), table=, nullable=False), Column('added_at', DateTime(), table=, nullable=False, default=CallableColumnDefault()), Column('is_training_data', Boolean(), table=, nullable=False, default=ScalarElementColumnDefault(True)), schema=None)]. Did you mean to say select(Table('training_data', MetaData(), Column('id', Integer(), table=, primary_key=True, nullable=False), Column('content', String(), table=, nullable=False), Column('content_type', String(), table=, nullable=False), Column('metadata', JSON(), table=, nullable=False), Column('added_at', DateTime(), table=, nullable=False, default=CallableColumnDefault()), Column('is_training_data', Boolean(), table=, nullable=False, default=ScalarElementColumnDefault(True)), schema=None))? +2025-04-18 18:47:11.756 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:47:11.756 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:47:12.100 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:47:12.100 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:47:21.198 | ERROR | main:get_training_data:306 - Error retrieving training data: Column expression, FROM clause, or other columns clause element expected, got [Table('training_data', MetaData(), Column('id', Integer(), table=, primary_key=True, nullable=False), Column('content', String(), table=, nullable=False), Column('content_type', String(), table=, nullable=False), Column('metadata', JSON(), table=, nullable=False), Column('added_at', DateTime(), table=, nullable=False, default=CallableColumnDefault()), Column('is_training_data', Boolean(), table=, nullable=False, default=ScalarElementColumnDefault(True)), schema=None)]. Did you mean to say select(Table('training_data', MetaData(), Column('id', Integer(), table=, primary_key=True, nullable=False), Column('content', String(), table=, nullable=False), Column('content_type', String(), table=, nullable=False), Column('metadata', JSON(), table=, nullable=False), Column('added_at', DateTime(), table=, nullable=False, default=CallableColumnDefault()), Column('is_training_data', Boolean(), table=, nullable=False, default=ScalarElementColumnDefault(True)), schema=None))? +2025-04-18 18:47:21.198 | ERROR | main:get_training_data:306 - Error retrieving training data: Column expression, FROM clause, or other columns clause element expected, got [Table('training_data', MetaData(), Column('id', Integer(), table=, primary_key=True, nullable=False), Column('content', String(), table=, nullable=False), Column('content_type', String(), table=, nullable=False), Column('metadata', JSON(), table=, nullable=False), Column('added_at', DateTime(), table=, nullable=False, default=CallableColumnDefault()), Column('is_training_data', Boolean(), table=, nullable=False, default=ScalarElementColumnDefault(True)), schema=None)]. Did you mean to say select(Table('training_data', MetaData(), Column('id', Integer(), table=, primary_key=True, nullable=False), Column('content', String(), table=, nullable=False), Column('content_type', String(), table=, nullable=False), Column('metadata', JSON(), table=, nullable=False), Column('added_at', DateTime(), table=, nullable=False, default=CallableColumnDefault()), Column('is_training_data', Boolean(), table=, nullable=False, default=ScalarElementColumnDefault(True)), schema=None))? +2025-04-18 18:48:22.623 | INFO | copywriter:_generate_headline_suggestions:188 - Generated 3 headline suggestions +2025-04-18 18:48:22.623 | INFO | copywriter:_generate_headline_suggestions:188 - Generated 3 headline suggestions +2025-04-18 18:48:22.636 | INFO | copywriter:generate_copy:90 - Generated content with 1802 characters +2025-04-18 18:48:22.636 | INFO | copywriter:generate_copy:90 - Generated content with 1802 characters +2025-04-18 18:48:23.411 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:48:23.411 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:48:46.063 | ERROR | main:delete_user_query:461 - Error deleting user query: 404: Query with timestamp 20250418153242 not found +2025-04-18 18:48:46.063 | ERROR | main:delete_user_query:461 - Error deleting user query: 404: Query with timestamp 20250418153242 not found +2025-04-18 18:49:03.774 | INFO | vector_store:delete_document:256 - Marked document 3 as deleted +2025-04-18 18:49:03.774 | INFO | vector_store:delete_document:256 - Marked document 3 as deleted +2025-04-18 18:49:06.850 | INFO | vector_store:delete_document:256 - Marked document 2 as deleted +2025-04-18 18:49:06.850 | INFO | vector_store:delete_document:256 - Marked document 2 as deleted +2025-04-18 18:49:11.443 | INFO | vector_store:delete_document:256 - Marked document 1 as deleted +2025-04-18 18:49:11.443 | INFO | vector_store:delete_document:256 - Marked document 1 as deleted +2025-04-18 18:51:53.774 | ERROR | main:add_training_data:214 - Error adding training data: no such table: training_data +2025-04-18 18:51:53.774 | ERROR | main:add_training_data:214 - Error adding training data: no such table: training_data +2025-04-18 18:52:03.005 | ERROR | main:add_training_data:214 - Error adding training data: no such table: training_data +2025-04-18 18:52:03.005 | ERROR | main:add_training_data:214 - Error adding training data: no such table: training_data +2025-04-18 18:52:49.739 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:52:49.739 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:52:55.266 | INFO | vector_store:delete_document:256 - Marked document 1 as deleted +2025-04-18 18:52:55.266 | INFO | vector_store:delete_document:256 - Marked document 1 as deleted +2025-04-18 18:56:30.386 | INFO | copywriter:_generate_headline_suggestions:188 - Generated 3 headline suggestions +2025-04-18 18:56:30.386 | INFO | copywriter:_generate_headline_suggestions:188 - Generated 3 headline suggestions +2025-04-18 18:56:30.388 | INFO | copywriter:generate_copy:90 - Generated content with 1580 characters +2025-04-18 18:56:30.388 | INFO | copywriter:generate_copy:90 - Generated content with 1580 characters +2025-04-18 18:56:30.997 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:56:30.997 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:57:15.893 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:57:15.893 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:58:37.976 | INFO | copywriter:_generate_headline_suggestions:188 - Generated 3 headline suggestions +2025-04-18 18:58:37.976 | INFO | copywriter:_generate_headline_suggestions:188 - Generated 3 headline suggestions +2025-04-18 18:58:37.980 | INFO | copywriter:generate_copy:90 - Generated content with 579 characters +2025-04-18 18:58:37.980 | INFO | copywriter:generate_copy:90 - Generated content with 579 characters +2025-04-18 18:58:38.798 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:58:38.798 | INFO | vector_store:add_documents:131 - Added 1 documents to vector store +2025-04-18 18:59:13.625 | INFO | copywriter:improve_copy:224 - Improved content based on feedback +2025-04-18 18:59:13.625 | INFO | copywriter:improve_copy:224 - Improved content based on feedback +2025-04-18 18:59:58.642 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 18:59:58.642 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 19:00:09.875 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 19:00:09.875 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 19:00:20.643 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 19:00:20.643 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 19:05:10.093 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines +2025-04-18 19:05:10.093 | INFO | brand_style:update_style_guidelines:178 - Updated brand style guidelines