diff --git a/backend/igny8_core/ai/tasks.py b/backend/igny8_core/ai/tasks.py index cfe436c1..7e173f04 100644 --- a/backend/igny8_core/ai/tasks.py +++ b/backend/igny8_core/ai/tasks.py @@ -536,9 +536,10 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None from django.conf import settings from pathlib import Path - # Create /data/app/images directory if it doesn't exist - # Try absolute path first, fallback to project-relative if needed - images_dir = '/data/app/images' + # Create images directory if it doesn't exist + # Use /data/app/igny8/images (mounted volume) for persistence + # Fallback to /data/app/images if mounted path not available + images_dir = '/data/app/igny8/images' # Use mounted volume path write_test_passed = False try: @@ -549,16 +550,13 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None f.write('test') os.remove(test_file) write_test_passed = True - logger.info(f"[process_image_generation_queue] Image {image_id} - Directory writable: {images_dir}") + logger.info(f"[process_image_generation_queue] Image {image_id} - Directory writable (mounted volume): {images_dir}") except Exception as write_test_error: - logger.warning(f"[process_image_generation_queue] Image {image_id} - Directory not writable: {images_dir}, error: {write_test_error}") - # Fallback to project-relative path - from django.conf import settings - base_dir = Path(settings.BASE_DIR) if hasattr(settings, 'BASE_DIR') else Path(__file__).resolve().parent.parent.parent - images_dir = str(base_dir / 'data' / 'app' / 'images') + logger.warning(f"[process_image_generation_queue] Image {image_id} - Mounted directory not writable: {images_dir}, error: {write_test_error}") + # Fallback to /data/app/images + images_dir = '/data/app/images' try: os.makedirs(images_dir, exist_ok=True) - # Test fallback directory write access test_file = os.path.join(images_dir, '.write_test') with open(test_file, 'w') as f: f.write('test') @@ -566,8 +564,22 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None write_test_passed = True logger.info(f"[process_image_generation_queue] Image {image_id} - Using fallback directory (writable): {images_dir}") except Exception as fallback_error: - logger.error(f"[process_image_generation_queue] Image {image_id} - Fallback directory also not writable: {images_dir}, error: {fallback_error}") - raise Exception(f"Neither /data/app/images nor {images_dir} is writable. Last error: {fallback_error}") + logger.warning(f"[process_image_generation_queue] Image {image_id} - Fallback directory also not writable: {images_dir}, error: {fallback_error}") + # Final fallback to project-relative path + from django.conf import settings + base_dir = Path(settings.BASE_DIR) if hasattr(settings, 'BASE_DIR') else Path(__file__).resolve().parent.parent.parent + images_dir = str(base_dir / 'data' / 'app' / 'images') + try: + os.makedirs(images_dir, exist_ok=True) + test_file = os.path.join(images_dir, '.write_test') + with open(test_file, 'w') as f: + f.write('test') + os.remove(test_file) + write_test_passed = True + logger.info(f"[process_image_generation_queue] Image {image_id} - Using project-relative directory (writable): {images_dir}") + except Exception as final_error: + logger.error(f"[process_image_generation_queue] Image {image_id} - All directories not writable. Last error: {final_error}") + raise Exception(f"None of the image directories are writable. Last error: {final_error}") if not write_test_passed: raise Exception(f"Failed to find writable directory for saving images") @@ -620,15 +632,29 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None logger.info(f"[process_image_generation_queue] Image {image_id} - URL length {len(image_url)} chars (was limited to 200, now supports 500)") try: - # Save file path if available, otherwise save URL + # Save file path and URL appropriately if saved_file_path: - # Store relative path or full path in image_url field - image.image_url = saved_file_path + # Store local file path in image_path field + image.image_path = saved_file_path + # Also keep the original URL in image_url field for reference + if image_url: + image.image_url = image_url + logger.info(f"[process_image_generation_queue] Image {image_id} - Saved local path: {saved_file_path}") else: + # Only URL available, save to image_url image.image_url = image_url + logger.info(f"[process_image_generation_queue] Image {image_id} - Saved URL only: {image_url[:100] if image_url else 'None'}...") image.status = 'generated' - logger.info(f"[process_image_generation_queue] Image {image_id} - Attempting to save to database") - image.save(update_fields=['image_url', 'status']) + + # Determine which fields to update + update_fields = ['status'] + if saved_file_path: + update_fields.append('image_path') + if image_url: + update_fields.append('image_url') + + logger.info(f"[process_image_generation_queue] Image {image_id} - Attempting to save to database (fields: {update_fields})") + image.save(update_fields=update_fields) logger.info(f"[process_image_generation_queue] Image {image_id} - Successfully saved to database") except Exception as save_error: error_str = str(save_error) @@ -659,7 +685,8 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None 'results': results + [{ 'image_id': image_id, 'status': 'completed', - 'image_url': saved_file_path or image_url, + 'image_url': image_url, # Original URL from API + 'image_path': saved_file_path, # Local file path if saved 'revised_prompt': result.get('revised_prompt') }] } @@ -668,7 +695,8 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None results.append({ 'image_id': image_id, 'status': 'completed', - 'image_url': saved_file_path or image_url, + 'image_url': image_url, # Original URL from API + 'image_path': saved_file_path, # Local file path if saved 'revised_prompt': result.get('revised_prompt') }) completed += 1 diff --git a/backend/igny8_core/modules/writer/views.py b/backend/igny8_core/modules/writer/views.py index 319cfffd..9476c3fb 100644 --- a/backend/igny8_core/modules/writer/views.py +++ b/backend/igny8_core/modules/writer/views.py @@ -367,6 +367,76 @@ class ImagesViewSet(SiteSectorModelViewSet): else: serializer.save() + @action(detail=True, methods=['get'], url_path='file', url_name='image_file') + def serve_image_file(self, request, pk=None): + """ + Serve image file from local path via URL + GET /api/v1/writer/images/{id}/file/ + """ + import os + from django.http import FileResponse, Http404 + from django.conf import settings + + try: + # Get image directly without account filtering for file serving + # This allows public access to image files + try: + image = Images.objects.get(pk=pk) + except Images.DoesNotExist: + return Response({ + 'error': 'Image not found' + }, status=status.HTTP_404_NOT_FOUND) + + # Check if image has a local path + if not image.image_path: + return Response({ + 'error': 'No local file path available for this image' + }, status=status.HTTP_404_NOT_FOUND) + + file_path = image.image_path + + # Verify file exists + if not os.path.exists(file_path): + return Response({ + 'error': f'Image file not found at: {file_path}' + }, status=status.HTTP_404_NOT_FOUND) + + # Check if file is readable + if not os.access(file_path, os.R_OK): + return Response({ + 'error': 'Image file is not readable' + }, status=status.HTTP_403_FORBIDDEN) + + # Determine content type from file extension + import mimetypes + content_type, _ = mimetypes.guess_type(file_path) + if not content_type: + content_type = 'image/png' # Default to PNG + + # Serve the file + try: + return FileResponse( + open(file_path, 'rb'), + content_type=content_type, + filename=os.path.basename(file_path) + ) + except Exception as e: + return Response({ + 'error': f'Failed to serve file: {str(e)}' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + except Images.DoesNotExist: + return Response({ + 'error': 'Image not found' + }, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.error(f"Error serving image file: {str(e)}", exc_info=True) + return Response({ + 'error': f'Failed to serve image: {str(e)}' + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + @action(detail=False, methods=['post'], url_path='auto_generate', url_name='auto_generate_images') def auto_generate_images(self, request): """Auto-generate images for tasks using AI""" diff --git a/frontend/node_modules/.tmp/tsconfig.app.tsbuildinfo b/frontend/node_modules/.tmp/tsconfig.app.tsbuildinfo index e29f6ea5..205eccac 100644 --- a/frontend/node_modules/.tmp/tsconfig.app.tsbuildinfo +++ b/frontend/node_modules/.tmp/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["../../src/App.tsx","../../src/main.tsx","../../src/svg.d.ts","../../src/vite-env.d.ts","../../src/components/UserProfile/UserAddressCard.tsx","../../src/components/UserProfile/UserInfoCard.tsx","../../src/components/UserProfile/UserMetaCard.tsx","../../src/components/auth/ProtectedRoute.tsx","../../src/components/auth/SignInForm.tsx","../../src/components/auth/SignUpForm.tsx","../../src/components/charts/bar/BarChartOne.tsx","../../src/components/charts/line/LineChartOne.tsx","../../src/components/common/BulkExportModal.tsx","../../src/components/common/BulkStatusUpdateModal.tsx","../../src/components/common/ChartTab.tsx","../../src/components/common/ComponentCard.tsx","../../src/components/common/ConfirmDialog.tsx","../../src/components/common/ErrorBoundary.tsx","../../src/components/common/FormModal.tsx","../../src/components/common/GlobalErrorDisplay.tsx","../../src/components/common/GridShape.tsx","../../src/components/common/HTMLContentRenderer.tsx","../../src/components/common/ImageGenerationCard.tsx","../../src/components/common/ImageResultCard.tsx","../../src/components/common/ImageServiceCard.tsx","../../src/components/common/IntegrationCard.tsx","../../src/components/common/LoadingStateMonitor.tsx","../../src/components/common/PageBreadCrumb.tsx","../../src/components/common/PageErrorBoundary.tsx","../../src/components/common/PageMeta.tsx","../../src/components/common/ProgressModal.tsx","../../src/components/common/ScrollToTop.tsx","../../src/components/common/SectorSelector.tsx","../../src/components/common/SiteCard.tsx","../../src/components/common/ThemeToggleButton.tsx","../../src/components/common/ThemeTogglerTwo.tsx","../../src/components/common/ToggleTableRow.tsx","../../src/components/common/ValidationCard.tsx","../../src/components/dashboard/CreditBalanceWidget.tsx","../../src/components/dashboard/UsageChartWidget.tsx","../../src/components/debug/ResourceDebugOverlay.tsx","../../src/components/debug/ResourceDebugToggle.tsx","../../src/components/ecommerce/CountryMap.tsx","../../src/components/ecommerce/DemographicCard.tsx","../../src/components/ecommerce/EcommerceMetrics.tsx","../../src/components/ecommerce/MonthlySalesChart.tsx","../../src/components/ecommerce/MonthlyTarget.tsx","../../src/components/ecommerce/RecentOrders.tsx","../../src/components/ecommerce/StatisticsChart.tsx","../../src/components/form/Form.tsx","../../src/components/form/FormFieldRenderer.tsx","../../src/components/form/Label.tsx","../../src/components/form/MultiSelect.tsx","../../src/components/form/Select.tsx","../../src/components/form/SelectDropdown.tsx","../../src/components/form/date-picker.tsx","../../src/components/form/form-elements/CheckboxComponents.tsx","../../src/components/form/form-elements/DefaultInputs.tsx","../../src/components/form/form-elements/DropZone.tsx","../../src/components/form/form-elements/FileInputExample.tsx","../../src/components/form/form-elements/InputGroup.tsx","../../src/components/form/form-elements/InputStates.tsx","../../src/components/form/form-elements/RadioButtons.tsx","../../src/components/form/form-elements/SelectInputs.tsx","../../src/components/form/form-elements/TextAreaInput.tsx","../../src/components/form/form-elements/ToggleSwitch.tsx","../../src/components/form/group-input/PhoneInput.tsx","../../src/components/form/input/Checkbox.tsx","../../src/components/form/input/FileInput.tsx","../../src/components/form/input/InputField.tsx","../../src/components/form/input/Radio.tsx","../../src/components/form/input/RadioSm.tsx","../../src/components/form/input/TextArea.tsx","../../src/components/form/switch/Switch.tsx","../../src/components/header/Header.tsx","../../src/components/header/HeaderMetrics.tsx","../../src/components/header/NotificationDropdown.tsx","../../src/components/header/SiteSwitcher.tsx","../../src/components/header/UserDropdown.tsx","../../src/components/tables/BasicTables/BasicTableOne.tsx","../../src/components/ui/alert/Alert.tsx","../../src/components/ui/alert/AlertModal.tsx","../../src/components/ui/avatar/Avatar.tsx","../../src/components/ui/badge/Badge.tsx","../../src/components/ui/breadcrumb/Breadcrumb.tsx","../../src/components/ui/breadcrumb/index.ts","../../src/components/ui/button/Button.tsx","../../src/components/ui/button-group/ButtonGroup.tsx","../../src/components/ui/button-group/index.ts","../../src/components/ui/card/Card.tsx","../../src/components/ui/card/index.tsx","../../src/components/ui/dropdown/Dropdown.tsx","../../src/components/ui/dropdown/DropdownItem.tsx","../../src/components/ui/images/ResponsiveImage.tsx","../../src/components/ui/images/ThreeColumnImageGrid.tsx","../../src/components/ui/images/TwoColumnImageGrid.tsx","../../src/components/ui/list/List.tsx","../../src/components/ui/list/index.tsx","../../src/components/ui/modal/index.tsx","../../src/components/ui/pagination/CompactPagination.tsx","../../src/components/ui/pagination/Pagination.tsx","../../src/components/ui/pagination/index.tsx","../../src/components/ui/pricing-table/PricingTable.tsx","../../src/components/ui/pricing-table/index.ts","../../src/components/ui/progress/ProgressBar.tsx","../../src/components/ui/progress/index.ts","../../src/components/ui/ribbon/Ribbon.tsx","../../src/components/ui/ribbon/index.ts","../../src/components/ui/spinner/Spinner.tsx","../../src/components/ui/spinner/index.ts","../../src/components/ui/table/index.tsx","../../src/components/ui/tabs/Tabs.tsx","../../src/components/ui/tabs/index.ts","../../src/components/ui/toast/Toast.tsx","../../src/components/ui/toast/ToastContainer.tsx","../../src/components/ui/tooltip/Tooltip.tsx","../../src/components/ui/tooltip/index.ts","../../src/components/ui/videos/AspectRatioVideo.tsx","../../src/components/ui/videos/FourIsToThree.tsx","../../src/components/ui/videos/OneIsToOne.tsx","../../src/components/ui/videos/SixteenIsToNine.tsx","../../src/components/ui/videos/TwentyOneIsToNine.tsx","../../src/config/import-export.config.tsx","../../src/config/routes.config.ts","../../src/config/version.ts","../../src/config/forms/keyword-form.config.ts","../../src/config/pages/bulk-action-modal.config.ts","../../src/config/pages/clusters.config.tsx","../../src/config/pages/delete-modal.config.ts","../../src/config/pages/ideas.config.tsx","../../src/config/pages/images.config.tsx","../../src/config/pages/keywords.config.tsx","../../src/config/pages/notifications.config.ts","../../src/config/pages/table-actions.config.tsx","../../src/config/pages/tasks.config.tsx","../../src/config/snippets/actions.snippets.ts","../../src/config/snippets/columns.snippets.ts","../../src/config/snippets/filters.snippets.ts","../../src/config/snippets/index.ts","../../src/context/HeaderMetricsContext.tsx","../../src/context/SidebarContext.tsx","../../src/context/ThemeContext.tsx","../../src/hooks/useErrorHandler.ts","../../src/hooks/useGoBack.ts","../../src/hooks/useModal.ts","../../src/hooks/usePageDataLoader.ts","../../src/hooks/usePersistentToggle.ts","../../src/hooks/useProgressModal.ts","../../src/icons/index.ts","../../src/layout/AppHeader.tsx","../../src/layout/AppLayout.tsx","../../src/layout/AppSidebar.tsx","../../src/layout/Backdrop.tsx","../../src/layout/SidebarWidget.tsx","../../src/pages/Analytics.tsx","../../src/pages/Blank.tsx","../../src/pages/Calendar.tsx","../../src/pages/Components.tsx","../../src/pages/Schedules.tsx","../../src/pages/UserProfiles.tsx","../../src/pages/AuthPages/AuthPageLayout.tsx","../../src/pages/AuthPages/SignIn.tsx","../../src/pages/AuthPages/SignUp.tsx","../../src/pages/Billing/Credits.tsx","../../src/pages/Billing/Transactions.tsx","../../src/pages/Billing/Usage.tsx","../../src/pages/Charts/BarChart.tsx","../../src/pages/Charts/LineChart.tsx","../../src/pages/Dashboard/Home.tsx","../../src/pages/Forms/FormElements.tsx","../../src/pages/Help/Docs.tsx","../../src/pages/Help/FunctionTesting.tsx","../../src/pages/Help/Help.tsx","../../src/pages/Help/SystemTesting.tsx","../../src/pages/OtherPage/NotFound.tsx","../../src/pages/Planner/Clusters.tsx","../../src/pages/Planner/Dashboard.tsx","../../src/pages/Planner/Ideas.tsx","../../src/pages/Planner/KeywordOpportunities.tsx","../../src/pages/Planner/Keywords.tsx","../../src/pages/Planner/Mapping.tsx","../../src/pages/Reference/Industries.tsx","../../src/pages/Reference/SeedKeywords.tsx","../../src/pages/Settings/AI.tsx","../../src/pages/Settings/Account.tsx","../../src/pages/Settings/General.tsx","../../src/pages/Settings/ImportExport.tsx","../../src/pages/Settings/Industries.tsx","../../src/pages/Settings/Integration.tsx","../../src/pages/Settings/Modules.tsx","../../src/pages/Settings/Plans.tsx","../../src/pages/Settings/Sites.tsx","../../src/pages/Settings/Status.tsx","../../src/pages/Settings/Subscriptions.tsx","../../src/pages/Settings/System.tsx","../../src/pages/Settings/Users.tsx","../../src/pages/Settings/UiElements/Alerts.tsx","../../src/pages/Settings/UiElements/Avatars.tsx","../../src/pages/Settings/UiElements/Badges.tsx","../../src/pages/Settings/UiElements/Breadcrumb.tsx","../../src/pages/Settings/UiElements/Buttons.tsx","../../src/pages/Settings/UiElements/ButtonsGroup.tsx","../../src/pages/Settings/UiElements/Cards.tsx","../../src/pages/Settings/UiElements/Carousel.tsx","../../src/pages/Settings/UiElements/Dropdowns.tsx","../../src/pages/Settings/UiElements/Images.tsx","../../src/pages/Settings/UiElements/Links.tsx","../../src/pages/Settings/UiElements/List.tsx","../../src/pages/Settings/UiElements/Modals.tsx","../../src/pages/Settings/UiElements/Notifications.tsx","../../src/pages/Settings/UiElements/Pagination.tsx","../../src/pages/Settings/UiElements/Popovers.tsx","../../src/pages/Settings/UiElements/PricingTable.tsx","../../src/pages/Settings/UiElements/Progressbar.tsx","../../src/pages/Settings/UiElements/Ribbons.tsx","../../src/pages/Settings/UiElements/Spinners.tsx","../../src/pages/Settings/UiElements/Tabs.tsx","../../src/pages/Settings/UiElements/Tooltips.tsx","../../src/pages/Settings/UiElements/Videos.tsx","../../src/pages/Tables/BasicTables.tsx","../../src/pages/Thinker/AuthorProfiles.tsx","../../src/pages/Thinker/Dashboard.tsx","../../src/pages/Thinker/ImageTesting.tsx","../../src/pages/Thinker/Profile.tsx","../../src/pages/Thinker/Prompts.tsx","../../src/pages/Thinker/Strategies.tsx","../../src/pages/Writer/Content.tsx","../../src/pages/Writer/Dashboard.tsx","../../src/pages/Writer/Drafts.tsx","../../src/pages/Writer/Images.tsx","../../src/pages/Writer/Published.tsx","../../src/pages/Writer/Tasks.tsx","../../src/services/api.ts","../../src/store/aiRequestLogsStore.ts","../../src/store/authStore.ts","../../src/store/billingStore.ts","../../src/store/pageSizeStore.ts","../../src/store/plannerStore.ts","../../src/store/sectorStore.ts","../../src/store/settingsStore.ts","../../src/store/siteStore.ts","../../src/templates/DashboardTemplate.tsx","../../src/templates/FormPageTemplate.tsx","../../src/templates/SystemPageTemplate.tsx","../../src/templates/TablePageTemplate.tsx","../../src/utils/date.ts","../../src/utils/difficulty.ts","../../src/utils/htmlSanitizer.ts","../../src/utils/index.ts","../../src/utils/table-import-export.ts"],"errors":true,"version":"5.7.3"} \ No newline at end of file +{"root":["../../src/App.tsx","../../src/main.tsx","../../src/svg.d.ts","../../src/vite-env.d.ts","../../src/components/UserProfile/UserAddressCard.tsx","../../src/components/UserProfile/UserInfoCard.tsx","../../src/components/UserProfile/UserMetaCard.tsx","../../src/components/auth/ProtectedRoute.tsx","../../src/components/auth/SignInForm.tsx","../../src/components/auth/SignUpForm.tsx","../../src/components/charts/bar/BarChartOne.tsx","../../src/components/charts/line/LineChartOne.tsx","../../src/components/common/BulkExportModal.tsx","../../src/components/common/BulkStatusUpdateModal.tsx","../../src/components/common/ChartTab.tsx","../../src/components/common/ComponentCard.tsx","../../src/components/common/ConfirmDialog.tsx","../../src/components/common/ContentImageCell.tsx","../../src/components/common/ErrorBoundary.tsx","../../src/components/common/FormModal.tsx","../../src/components/common/GlobalErrorDisplay.tsx","../../src/components/common/GridShape.tsx","../../src/components/common/HTMLContentRenderer.tsx","../../src/components/common/ImageGenerationCard.tsx","../../src/components/common/ImageQueueModal.tsx","../../src/components/common/ImageResultCard.tsx","../../src/components/common/ImageServiceCard.tsx","../../src/components/common/IntegrationCard.tsx","../../src/components/common/LoadingStateMonitor.tsx","../../src/components/common/PageBreadCrumb.tsx","../../src/components/common/PageErrorBoundary.tsx","../../src/components/common/PageMeta.tsx","../../src/components/common/PageTransition.tsx","../../src/components/common/ProgressModal.tsx","../../src/components/common/ScrollToTop.tsx","../../src/components/common/SectorSelector.tsx","../../src/components/common/SiteCard.tsx","../../src/components/common/ThemeToggleButton.tsx","../../src/components/common/ThemeTogglerTwo.tsx","../../src/components/common/ToggleTableRow.tsx","../../src/components/common/ValidationCard.tsx","../../src/components/dashboard/CreditBalanceWidget.tsx","../../src/components/dashboard/UsageChartWidget.tsx","../../src/components/debug/ResourceDebugOverlay.tsx","../../src/components/debug/ResourceDebugToggle.tsx","../../src/components/ecommerce/CountryMap.tsx","../../src/components/ecommerce/DemographicCard.tsx","../../src/components/ecommerce/EcommerceMetrics.tsx","../../src/components/ecommerce/MonthlySalesChart.tsx","../../src/components/ecommerce/MonthlyTarget.tsx","../../src/components/ecommerce/RecentOrders.tsx","../../src/components/ecommerce/StatisticsChart.tsx","../../src/components/form/Form.tsx","../../src/components/form/FormFieldRenderer.tsx","../../src/components/form/Label.tsx","../../src/components/form/MultiSelect.tsx","../../src/components/form/Select.tsx","../../src/components/form/SelectDropdown.tsx","../../src/components/form/date-picker.tsx","../../src/components/form/form-elements/CheckboxComponents.tsx","../../src/components/form/form-elements/DefaultInputs.tsx","../../src/components/form/form-elements/DropZone.tsx","../../src/components/form/form-elements/FileInputExample.tsx","../../src/components/form/form-elements/InputGroup.tsx","../../src/components/form/form-elements/InputStates.tsx","../../src/components/form/form-elements/RadioButtons.tsx","../../src/components/form/form-elements/SelectInputs.tsx","../../src/components/form/form-elements/TextAreaInput.tsx","../../src/components/form/form-elements/ToggleSwitch.tsx","../../src/components/form/group-input/PhoneInput.tsx","../../src/components/form/input/Checkbox.tsx","../../src/components/form/input/FileInput.tsx","../../src/components/form/input/InputField.tsx","../../src/components/form/input/Radio.tsx","../../src/components/form/input/RadioSm.tsx","../../src/components/form/input/TextArea.tsx","../../src/components/form/switch/Switch.tsx","../../src/components/header/Header.tsx","../../src/components/header/HeaderMetrics.tsx","../../src/components/header/NotificationDropdown.tsx","../../src/components/header/SiteSwitcher.tsx","../../src/components/header/UserDropdown.tsx","../../src/components/tables/BasicTables/BasicTableOne.tsx","../../src/components/ui/alert/Alert.tsx","../../src/components/ui/alert/AlertModal.tsx","../../src/components/ui/avatar/Avatar.tsx","../../src/components/ui/badge/Badge.tsx","../../src/components/ui/breadcrumb/Breadcrumb.tsx","../../src/components/ui/breadcrumb/index.ts","../../src/components/ui/button/Button.tsx","../../src/components/ui/button-group/ButtonGroup.tsx","../../src/components/ui/button-group/index.ts","../../src/components/ui/card/Card.tsx","../../src/components/ui/card/index.tsx","../../src/components/ui/dropdown/Dropdown.tsx","../../src/components/ui/dropdown/DropdownItem.tsx","../../src/components/ui/images/ResponsiveImage.tsx","../../src/components/ui/images/ThreeColumnImageGrid.tsx","../../src/components/ui/images/TwoColumnImageGrid.tsx","../../src/components/ui/list/List.tsx","../../src/components/ui/list/index.tsx","../../src/components/ui/modal/index.tsx","../../src/components/ui/pagination/CompactPagination.tsx","../../src/components/ui/pagination/Pagination.tsx","../../src/components/ui/pagination/index.tsx","../../src/components/ui/pricing-table/PricingTable.tsx","../../src/components/ui/pricing-table/index.ts","../../src/components/ui/progress/ProgressBar.tsx","../../src/components/ui/progress/index.ts","../../src/components/ui/ribbon/Ribbon.tsx","../../src/components/ui/ribbon/index.ts","../../src/components/ui/spinner/Spinner.tsx","../../src/components/ui/spinner/index.ts","../../src/components/ui/table/index.tsx","../../src/components/ui/tabs/Tabs.tsx","../../src/components/ui/tabs/index.ts","../../src/components/ui/toast/Toast.tsx","../../src/components/ui/toast/ToastContainer.tsx","../../src/components/ui/tooltip/Tooltip.tsx","../../src/components/ui/tooltip/index.ts","../../src/components/ui/videos/AspectRatioVideo.tsx","../../src/components/ui/videos/FourIsToThree.tsx","../../src/components/ui/videos/OneIsToOne.tsx","../../src/components/ui/videos/SixteenIsToNine.tsx","../../src/components/ui/videos/TwentyOneIsToNine.tsx","../../src/config/import-export.config.tsx","../../src/config/routes.config.ts","../../src/config/version.ts","../../src/config/forms/keyword-form.config.ts","../../src/config/pages/bulk-action-modal.config.ts","../../src/config/pages/clusters.config.tsx","../../src/config/pages/content.config.tsx","../../src/config/pages/delete-modal.config.ts","../../src/config/pages/ideas.config.tsx","../../src/config/pages/images.config.tsx","../../src/config/pages/keywords.config.tsx","../../src/config/pages/notifications.config.ts","../../src/config/pages/table-actions.config.tsx","../../src/config/pages/tasks.config.tsx","../../src/config/snippets/actions.snippets.ts","../../src/config/snippets/columns.snippets.ts","../../src/config/snippets/filters.snippets.ts","../../src/config/snippets/index.ts","../../src/context/HeaderMetricsContext.tsx","../../src/context/SidebarContext.tsx","../../src/context/ThemeContext.tsx","../../src/hooks/useErrorHandler.ts","../../src/hooks/useGoBack.ts","../../src/hooks/useModal.ts","../../src/hooks/usePageDataLoader.ts","../../src/hooks/usePersistentToggle.ts","../../src/hooks/useProgressModal.ts","../../src/hooks/useResourceDebug.ts","../../src/icons/index.ts","../../src/icons/lazy.ts","../../src/layout/AppHeader.tsx","../../src/layout/AppLayout.tsx","../../src/layout/AppSidebar.tsx","../../src/layout/Backdrop.tsx","../../src/layout/SidebarWidget.tsx","../../src/pages/Analytics.tsx","../../src/pages/Blank.tsx","../../src/pages/Calendar.tsx","../../src/pages/Components.tsx","../../src/pages/Schedules.tsx","../../src/pages/UserProfiles.tsx","../../src/pages/AuthPages/AuthPageLayout.tsx","../../src/pages/AuthPages/SignIn.tsx","../../src/pages/AuthPages/SignUp.tsx","../../src/pages/Billing/Credits.tsx","../../src/pages/Billing/Transactions.tsx","../../src/pages/Billing/Usage.tsx","../../src/pages/Charts/BarChart.tsx","../../src/pages/Charts/LineChart.tsx","../../src/pages/Dashboard/Home.tsx","../../src/pages/Forms/FormElements.tsx","../../src/pages/Help/Docs.tsx","../../src/pages/Help/FunctionTesting.tsx","../../src/pages/Help/Help.tsx","../../src/pages/Help/SystemTesting.tsx","../../src/pages/OtherPage/NotFound.tsx","../../src/pages/Planner/Clusters.tsx","../../src/pages/Planner/Dashboard.tsx","../../src/pages/Planner/Ideas.tsx","../../src/pages/Planner/KeywordOpportunities.tsx","../../src/pages/Planner/Keywords.tsx","../../src/pages/Planner/Mapping.tsx","../../src/pages/Reference/Industries.tsx","../../src/pages/Reference/SeedKeywords.tsx","../../src/pages/Settings/AI.tsx","../../src/pages/Settings/Account.tsx","../../src/pages/Settings/General.tsx","../../src/pages/Settings/ImportExport.tsx","../../src/pages/Settings/Industries.tsx","../../src/pages/Settings/Integration.tsx","../../src/pages/Settings/Modules.tsx","../../src/pages/Settings/Plans.tsx","../../src/pages/Settings/Sites.tsx","../../src/pages/Settings/Status.tsx","../../src/pages/Settings/Subscriptions.tsx","../../src/pages/Settings/System.tsx","../../src/pages/Settings/Users.tsx","../../src/pages/Settings/UiElements/Alerts.tsx","../../src/pages/Settings/UiElements/Avatars.tsx","../../src/pages/Settings/UiElements/Badges.tsx","../../src/pages/Settings/UiElements/Breadcrumb.tsx","../../src/pages/Settings/UiElements/Buttons.tsx","../../src/pages/Settings/UiElements/ButtonsGroup.tsx","../../src/pages/Settings/UiElements/Cards.tsx","../../src/pages/Settings/UiElements/Carousel.tsx","../../src/pages/Settings/UiElements/Dropdowns.tsx","../../src/pages/Settings/UiElements/Images.tsx","../../src/pages/Settings/UiElements/Links.tsx","../../src/pages/Settings/UiElements/List.tsx","../../src/pages/Settings/UiElements/Modals.tsx","../../src/pages/Settings/UiElements/Notifications.tsx","../../src/pages/Settings/UiElements/Pagination.tsx","../../src/pages/Settings/UiElements/Popovers.tsx","../../src/pages/Settings/UiElements/PricingTable.tsx","../../src/pages/Settings/UiElements/Progressbar.tsx","../../src/pages/Settings/UiElements/Ribbons.tsx","../../src/pages/Settings/UiElements/Spinners.tsx","../../src/pages/Settings/UiElements/Tabs.tsx","../../src/pages/Settings/UiElements/Tooltips.tsx","../../src/pages/Settings/UiElements/Videos.tsx","../../src/pages/Tables/BasicTables.tsx","../../src/pages/Thinker/AuthorProfiles.tsx","../../src/pages/Thinker/Dashboard.tsx","../../src/pages/Thinker/ImageTesting.tsx","../../src/pages/Thinker/Profile.tsx","../../src/pages/Thinker/Prompts.tsx","../../src/pages/Thinker/Strategies.tsx","../../src/pages/Writer/Content.tsx","../../src/pages/Writer/ContentView.tsx","../../src/pages/Writer/Dashboard.tsx","../../src/pages/Writer/Drafts.tsx","../../src/pages/Writer/Images.tsx","../../src/pages/Writer/Published.tsx","../../src/pages/Writer/Tasks.tsx","../../src/services/api.ts","../../src/store/authStore.ts","../../src/store/billingStore.ts","../../src/store/pageSizeStore.ts","../../src/store/plannerStore.ts","../../src/store/sectorStore.ts","../../src/store/settingsStore.ts","../../src/store/siteStore.ts","../../src/templates/ContentViewTemplate.tsx","../../src/templates/DashboardTemplate.tsx","../../src/templates/FormPageTemplate.tsx","../../src/templates/SystemPageTemplate.tsx","../../src/templates/TablePageTemplate.tsx","../../src/utils/date.ts","../../src/utils/difficulty.ts","../../src/utils/htmlSanitizer.ts","../../src/utils/index.ts","../../src/utils/table-import-export.ts"],"errors":true,"version":"5.7.3"} \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 476a45fe..04ec4ef9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,6 +25,7 @@ const KeywordOpportunities = lazy(() => import("./pages/Planner/KeywordOpportuni const WriterDashboard = lazy(() => import("./pages/Writer/Dashboard")); const Tasks = lazy(() => import("./pages/Writer/Tasks")); const Content = lazy(() => import("./pages/Writer/Content")); +const ContentView = lazy(() => import("./pages/Writer/ContentView")); const Drafts = lazy(() => import("./pages/Writer/Drafts")); const Images = lazy(() => import("./pages/Writer/Images")); const Published = lazy(() => import("./pages/Writer/Published")); @@ -159,11 +160,18 @@ export default function App() { } /> + {/* Writer Content Routes - Order matters: list route must come before detail route */} } /> + {/* Content detail view - matches /writer/content/:id (e.g., /writer/content/10) */} + + + + } /> } /> diff --git a/frontend/src/pages/Writer/ContentView.tsx b/frontend/src/pages/Writer/ContentView.tsx new file mode 100644 index 00000000..c79f7506 --- /dev/null +++ b/frontend/src/pages/Writer/ContentView.tsx @@ -0,0 +1,71 @@ +/** + * ContentView Page - Displays individual content using ContentViewTemplate + * Route: /writer/content/:id + */ + +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router'; +import ContentViewTemplate from '../../templates/ContentViewTemplate'; +import { fetchContentById, Content } from '../../services/api'; +import { useToast } from '../../components/ui/toast/ToastContainer'; +import PageMeta from '../../components/common/PageMeta'; + +export default function ContentView() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const toast = useToast(); + + const [content, setContent] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadContent = async () => { + // Validate ID parameter - must be a number + if (!id) { + toast.error('Content ID is required'); + navigate('/writer/content'); + return; + } + + const contentId = parseInt(id, 10); + if (isNaN(contentId) || contentId <= 0) { + toast.error('Invalid content ID'); + navigate('/writer/content'); + return; + } + + setLoading(true); + try { + const data = await fetchContentById(contentId); + setContent(data); + } catch (error: any) { + console.error('Error loading content:', error); + toast.error(`Failed to load content: ${error.message || 'Unknown error'}`); + setContent(null); + } finally { + setLoading(false); + } + }; + + loadContent(); + }, [id, navigate, toast]); + + const handleBack = () => { + navigate('/writer/content'); + }; + + return ( + <> + + + + ); +} + diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 188e72c4..2f0e357e 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1589,3 +1589,7 @@ export async function fetchContent(filters: ContentFilters = {}): Promise { + return fetchAPI(`/v1/writer/content/${id}/`); +} + diff --git a/frontend/src/templates/ContentViewTemplate.tsx b/frontend/src/templates/ContentViewTemplate.tsx new file mode 100644 index 00000000..47633caf --- /dev/null +++ b/frontend/src/templates/ContentViewTemplate.tsx @@ -0,0 +1,393 @@ +/** + * ContentViewTemplate - Template for displaying individual content with all metadata + * + * Features: + * - Centered layout with max-width 1200px + * - Modern styling with Tailwind CSS + * - Displays all content metadata + * - Responsive design + * + * Usage: + * navigate('/writer/content')} + * /> + */ + +import React from 'react'; +import { Content } from '../services/api'; +import { ArrowLeftIcon, CalendarIcon, TagIcon, FileTextIcon, CheckCircleIcon, XCircleIcon, ClockIcon } from '../icons'; + +interface ContentViewTemplateProps { + content: Content | null; + loading: boolean; + onBack?: () => void; +} + +export default function ContentViewTemplate({ content, loading, onBack }: ContentViewTemplateProps) { + if (loading) { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+ ); + } + + if (!content) { + return ( +
+
+
+ +

Content Not Found

+

The content you're looking for doesn't exist or has been deleted.

+ {onBack && ( + + )} +
+
+
+ ); + } + + const formatDate = (dateString: string) => { + try { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + } catch { + return dateString; + } + }; + + const getStatusColor = (status: string) => { + const statusLower = status.toLowerCase(); + if (statusLower === 'generated' || statusLower === 'published' || statusLower === 'complete') { + return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'; + } + if (statusLower === 'pending' || statusLower === 'draft') { + return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400'; + } + if (statusLower === 'failed' || statusLower === 'error') { + return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'; + } + return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'; + }; + + return ( +
+
+ {/* Back Button */} + {onBack && ( + + )} + + {/* Main Content Card */} +
+ {/* Header Section */} +
+
+
+
+ + + {content.status} + +
+

+ {content.meta_title || content.title || `Content #${content.id}`} +

+ {content.meta_description && ( +

+ {content.meta_description} +

+ )} +
+
+
+ + {/* Metadata Section */} +
+
+ {/* Basic Info */} +
+

+ Basic Information +

+
+
+ +
+

Generated

+

+ {formatDate(content.generated_at)} +

+
+
+ {content.updated_at && content.updated_at !== content.generated_at && ( +
+ +
+

Last Updated

+

+ {formatDate(content.updated_at)} +

+
+
+ )} +
+ +
+

Word Count

+

+ {content.word_count?.toLocaleString() || 'N/A'} words +

+
+
+
+
+ + {/* Task & Sector Info */} +
+

+ Related Information +

+
+ {content.task_title && ( +
+

Task

+

+ {content.task_title} +

+

ID: {content.task_id}

+
+ )} + {content.sector_name && ( +
+

Sector

+

+ {content.sector_name} +

+
+ )} +
+
+ + {/* Keywords & Tags */} +
+

+ SEO & Tags +

+
+ {content.primary_keyword && ( +
+

Primary Keyword

+

+ {content.primary_keyword} +

+
+ )} + {content.secondary_keywords && content.secondary_keywords.length > 0 && ( +
+

Secondary Keywords

+
+ {content.secondary_keywords.map((keyword, idx) => ( + + {keyword} + + ))} +
+
+ )} +
+
+
+ + {/* Tags and Categories */} + {(content.tags && content.tags.length > 0) || (content.categories && content.categories.length > 0) ? ( +
+
+ {content.tags && content.tags.length > 0 && ( +
+ +
+ {content.tags.map((tag, idx) => ( + + {tag} + + ))} +
+
+ )} + {content.categories && content.categories.length > 0 && ( +
+ Categories: +
+ {content.categories.map((category, idx) => ( + + {category} + + ))} +
+
+ )} +
+
+ ) : null} +
+ + {/* Image Status */} + {(content.has_image_prompts || content.has_generated_images) && ( +
+
+ {content.has_image_prompts && ( +
+ + Image Prompts Generated +
+ )} + {content.has_generated_images && ( +
+ + Images Generated +
+ )} +
+
+ )} + + {/* HTML Content */} +
+
+
+
+
+ + {/* Metadata JSON (Collapsible) */} + {content.metadata && Object.keys(content.metadata).length > 0 && ( +
+
+ + View Full Metadata + +
+
+                    {JSON.stringify(content.metadata, null, 2)}
+                  
+
+
+
+ )} +
+
+ + {/* Custom Styles for Content HTML */} + +
+ ); +} + diff --git a/images/img-y7hooMkdjGUoscfAKHonJKYO.png b/images/img-y7hooMkdjGUoscfAKHonJKYO.png new file mode 100644 index 00000000..a6faacfe Binary files /dev/null and b/images/img-y7hooMkdjGUoscfAKHonJKYO.png differ