From 178b7c23ce9e96080928f755e9b4ca691bbc58a7 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sat, 27 Dec 2025 02:43:46 +0000 Subject: [PATCH] Section 3 Completed --- CHANGELOG.md | 95 ++- docs/20-API/ENDPOINTS.md | 6 + docs/30-FRONTEND/PAGES.md | 48 +- frontend/src/App.tsx | 2 - .../src/components/common/ProgressModal.tsx | 10 +- frontend/src/config/pages/clusters.config.tsx | 12 +- frontend/src/config/pages/ideas.config.tsx | 15 +- .../pages/Planner/KeywordOpportunities.tsx | 680 ------------------ .../src/templates/ContentViewTemplate.tsx | 42 -- to-do-s/part1/SECTION_2_FINAL_MODS.md | 47 ++ to-do-s/part1/SECTION_3_FINAL_MODS.md | 40 +- 11 files changed, 242 insertions(+), 755 deletions(-) delete mode 100644 frontend/src/pages/Planner/KeywordOpportunities.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index a6bdcd12..b5621a5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # IGNY8 Change Log -**Current Version:** 1.1.3 -**Last Updated:** December 27, 2025 +**Current Version:** 1.1.5 +**Last Updated:** January 2, 2025 --- @@ -9,6 +9,8 @@ | Version | Date | Summary | |---------|------|---------| +| 1.1.5 | Jan 2, 2025 | Section 3 WORKFLOW modules - Planner, Writer, Progress Modal fixes | +| 1.1.4 | Dec 27, 2025 | Section 2 SETUP modules - Add Keywords, Content Settings, Sites fixes | | 1.1.3 | Dec 27, 2025 | Merged RULES.md into .rules | | 1.1.2 | Dec 27, 2025 | Module status documentation, TODOS.md | | 1.1.1 | Dec 27, 2025 | Simplified AI agent rules file | @@ -22,6 +24,95 @@ --- +## v1.1.5 - January 2, 2025 + +### Section 3 WORKFLOW Modules Implementation + +**Planner Module:** +- **DELETED** `KeywordOpportunities.tsx` - orphaned page, Add Keywords is source of truth +- Removed route `/planner/keyword-opportunities` from App.tsx +- Updated Clusters table to show ideas count badge: "X ideas" (green) or "No ideas" (gray) +- Made cluster name clickable in Ideas table - navigates to `/planner/clusters/:id` + +**Writer Module:** +- Fixed duplicate tags/categories display in ContentView template + - Removed redundant tags/categories section that appeared below the main metadata + - Tags and categories now display only once in the Topic section + +**Progress Modals:** +- Fixed placeholder "X" text in image prompt progress modals: + - Changed "Mapping Content for X Image Prompts" → "Mapping content for image prompts" + - Changed "Writing X In‑article Image Prompts" → "Writing In‑article Image Prompts" + - Changed "Featured Image and X In‑article..." → "Image prompts ready for generation" +- All step labels now show actual counts when available, clean fallbacks otherwise + +### Files Changed +- `frontend/src/App.tsx` - Removed KeywordOpportunities import/route +- `frontend/src/config/pages/clusters.config.tsx` - Ideas count badge styling +- `frontend/src/config/pages/ideas.config.tsx` - Clickable cluster link +- `frontend/src/templates/ContentViewTemplate.tsx` - Removed duplicate tags/categories +- `frontend/src/components/common/ProgressModal.tsx` - Fixed placeholder texts +- `docs/30-FRONTEND/PAGES.md` - Removed KeywordOpportunities references, updated version +- `docs/20-API/ENDPOINTS.md` - Added Content Settings API documentation + +### Files Deleted +- `frontend/src/pages/Planner/KeywordOpportunities.tsx` + +--- + +## v1.1.4 - December 27, 2025 + +### Section 2 SETUP Modules Implementation + +**Add Keywords Page (`/setup/add-keywords`):** +- Added "Not Yet Added Only" filter toggle to show only keywords not in workflow +- Added keyword count summary: "X keywords in your workflow • Y available to add" +- Added "Next: Plan Your Content →" CTA button (appears after adding keywords) +- Added "Keyword Research coming soon!" teaser text +- Sector requirement tooltip already existed - no changes needed + +**Content Settings Page (`/account/content-settings`):** +- **NEW Backend API**: Created `/v1/system/settings/content//` endpoints + - `GET /v1/system/settings/content/content_generation/` - Retrieve settings + - `POST /v1/system/settings/content/content_generation/save/` - Save settings + - `GET /v1/system/settings/content/publishing/` - Retrieve settings + - `POST /v1/system/settings/content/publishing/save/` - Save settings +- Content Generation tab now persists: append_to_prompt, default_tone, default_length +- Publishing tab now persists: auto_publish_enabled, auto_sync_enabled +- Fixed false "saved" confirmation - now actually saves to backend + +**Sites Module:** +- **NEW Component**: Created `SiteSetupChecklist` component showing setup progress + - Displays checklist: Site created, Industry/Sectors, WordPress integration, Keywords + - Progress bar with percentage + - "Complete Setup" button linking to first incomplete item + - "Ready to create content!" message when all complete +- Updated Site Dashboard to use SiteSetupChecklist instead of mock stats +- Removed mock statistics that showed all zeros + +**Cleanup:** +- Deleted `pages/Sites/Manage.tsx` (redundant duplicate of List.tsx) +- Removed empty `pages/Sites/Builder/` folder structure +- Removed `/sites/manage` route from App.tsx +- Removed SiteManage lazy import from App.tsx + +### Files Changed +- `frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx` +- `frontend/src/pages/account/ContentSettingsPage.tsx` +- `frontend/src/pages/Sites/Dashboard.tsx` +- `frontend/src/App.tsx` +- `backend/igny8_core/modules/system/settings_views.py` +- `backend/igny8_core/modules/system/urls.py` + +### Files Created +- `frontend/src/components/sites/SiteSetupChecklist.tsx` + +### Files Deleted +- `frontend/src/pages/Sites/Manage.tsx` +- `frontend/src/pages/Sites/Builder/` (empty directory) + +--- + ## v1.1.3 - December 27, 2025 ### Changed diff --git a/docs/20-API/ENDPOINTS.md b/docs/20-API/ENDPOINTS.md index a1179b88..72dd0528 100644 --- a/docs/20-API/ENDPOINTS.md +++ b/docs/20-API/ENDPOINTS.md @@ -135,6 +135,8 @@ All endpoints require authentication unless noted. | GET | `/settings/integrations/image_generation/` | Get image settings | Current config | | PUT | `/settings/integrations/image_generation/` | Save image settings | Update config | | POST | `/settings/integrations/test/` | Test connection | Verify API keys | +| GET | `/settings/content/{key}/` | `ContentSettingsViewSet.retrieve` | Get content settings | +| POST | `/settings/content/{key}/save/` | `ContentSettingsViewSet.save_settings` | Save content settings | | GET | `/prompts/` | List prompts | All prompts | | GET | `/prompts/{type}/` | Get prompt | Specific prompt | | PUT | `/prompts/{type}/` | Save prompt | Update prompt | @@ -143,6 +145,10 @@ All endpoints require authentication unless noted. | PUT | `/modules/` | Save modules | Update enabled | | GET | `/health/` | Health check | System status | +**Content Settings Keys:** +- `content_generation` - AI writing settings (default_article_length, default_tone, include_faq, enable_internal_linking, etc.) +- `publishing` - Publishing defaults (default_publish_status, auto_schedule, schedule_frequency, schedule_times) + --- ## Automation Endpoints (`/api/v1/automation/`) diff --git a/docs/30-FRONTEND/PAGES.md b/docs/30-FRONTEND/PAGES.md index dd481cbd..a472ad2a 100644 --- a/docs/30-FRONTEND/PAGES.md +++ b/docs/30-FRONTEND/PAGES.md @@ -1,7 +1,7 @@ # Frontend Pages & Routes -**Last Verified:** December 25, 2025 -**Version:** 1.1.0 +**Last Verified:** January 2, 2025 +**Version:** 1.1.4 **Framework:** React 19 + TypeScript + React Router 6 + Vite --- @@ -41,7 +41,14 @@ Routes defined in `/frontend/src/App.tsx`: | Route | File | Description | |-------|------|-------------| -| `/setup/add-keywords` | `Setup/AddKeywords.tsx` | Browse/add seed keywords from global database | +| `/setup/add-keywords` | `Setup/IndustriesSectorsKeywords.tsx` | Browse/add seed keywords from global database | + +**Features:** +- Industry → Sector → Keywords browse hierarchy +- "Show not-added only" filter toggle +- Real-time keyword count summary (added/total) +- "Next: Plan Your Content" CTA button +- "Keyword Research" coming soon teaser ### Content Settings @@ -49,14 +56,18 @@ Routes defined in `/frontend/src/App.tsx`: |-------|------|-------------| | `/account/content-settings` | `account/ContentSettingsPage.tsx` | 3 tabs: Content Generation, Publishing, Image Settings | +**API Endpoints:** Settings persisted via `/api/v1/system/settings/content/{key}/` + ### Sites | Route | File | Description | |-------|------|-------------| -| `/sites` | `Sites/List.tsx` | Site listing with filters | -| `/sites/:id/dashboard` | `Sites/SiteDashboard.tsx` | Individual site overview | -| `/sites/:id/settings` | `Sites/SiteSettings.tsx` | Site settings (General, Integrations, Content Types) | -| `/sites/:id/content` | `Sites/SiteContent.tsx` | Site content management | +| `/sites` | `Sites/List.tsx` | Site listing with setup checklist per site | +| `/sites/:id/dashboard` | `Sites/Dashboard.tsx` | Site overview with setup checklist | +| `/sites/:id/settings` | `Sites/Settings.tsx` | Site settings (General, Integrations, Content Types) | + +**Components:** +- `SiteSetupChecklist` - Shows setup progress (site created, industry/sectors, WordPress, keywords) ### Thinker (Admin Only) @@ -80,7 +91,6 @@ Routes defined in `/frontend/src/App.tsx`: | `/planner/clusters` | `Planner/Clusters.tsx` | Cluster listing, AI clustering | Clusters | | `/planner/clusters/:id` | `Planner/ClusterView.tsx` | Individual cluster view | - | | `/planner/ideas` | `Planner/Ideas.tsx` | Content ideas, queue to writer | Ideas | -| `/planner/keyword-opportunities` | `Planner/KeywordOpportunities.tsx` | Seed keyword discovery (hidden) | - | ### Writer @@ -228,18 +238,15 @@ frontend/src/pages/ │ ├── Keywords.tsx │ ├── Clusters.tsx │ ├── ClusterView.tsx -│ ├── Ideas.tsx -│ └── KeywordOpportunities.tsx # Not in nav +│ └── Ideas.tsx ├── Settings/ │ └── IntegrationPage.tsx # AI Models (admin) ├── Setup/ -│ └── AddKeywords.tsx +│ └── IndustriesSectorsKeywords.tsx # Add Keywords page ├── Sites/ -│ ├── List.tsx -│ ├── SiteDashboard.tsx -│ ├── SiteSettings.tsx -│ ├── SiteContent.tsx -│ └── Manage.tsx # Possibly redundant +│ ├── List.tsx # Site listing +│ ├── Dashboard.tsx # Site overview + checklist +│ └── Settings.tsx # Site configuration ├── Thinker/ │ ├── Prompts.tsx │ ├── AuthorProfiles.tsx @@ -303,10 +310,9 @@ Dashboard ## Known Issues (from Audit) -1. **KeywordOpportunities** not accessible from navigation -2. **Linker/Optimizer Dashboards** exist but not exposed -3. **Help sub-pages** are placeholders -4. **ContentView** is read-only (no editing capability) -5. Legacy redirects may cause confusion +1. **Linker/Optimizer Dashboards** exist but not exposed in navigation +2. **Help sub-pages** are placeholders +3. **ContentView** is read-only (no editing capability) +4. Legacy redirects may cause confusion See `/PRE-LAUNCH-AUDIT.md` for complete issue list. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b530eaf2..217c0435 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,7 +25,6 @@ const Keywords = lazy(() => import("./pages/Planner/Keywords")); const Clusters = lazy(() => import("./pages/Planner/Clusters")); const ClusterDetail = lazy(() => import("./pages/Planner/ClusterDetail")); const Ideas = lazy(() => import("./pages/Planner/Ideas")); -const KeywordOpportunities = lazy(() => import("./pages/Planner/KeywordOpportunities")); // Writer Module - Lazy loaded const WriterDashboard = lazy(() => import("./pages/Writer/Dashboard")); @@ -203,7 +202,6 @@ export default function App() { {/* Reference Data */} } /> - } /> } /> {/* Setup Pages */} diff --git a/frontend/src/components/common/ProgressModal.tsx b/frontend/src/components/common/ProgressModal.tsx index a66d41c8..1e9ed1dc 100644 --- a/frontend/src/components/common/ProgressModal.tsx +++ b/frontend/src/components/common/ProgressModal.tsx @@ -127,7 +127,7 @@ const getSuccessMessage = (functionId?: string, title?: string, stepLogs?: any[] } // Default message - return 'Featured Image and X In‑article Image Prompts ready for image generation'; + return 'Image prompts ready for generation'; } return 'Task completed successfully.'; }; @@ -180,9 +180,9 @@ const getStepsForFunction = (functionId?: string, title?: string): Array<{phase: // Image prompt generation return [ { phase: 'INIT', label: 'Checking content and image slots' }, - { phase: 'PREP', label: 'Mapping Content for X Image Prompts' }, + { phase: 'PREP', label: 'Mapping content for image prompts' }, { phase: 'AI_CALL', label: 'Writing Featured Image Prompts' }, - { phase: 'PARSE', label: 'Writing X In‑article Image Prompts' }, + { phase: 'PARSE', label: 'Writing In‑article Image Prompts' }, { phase: 'SAVE', label: 'Assigning Prompts to Dedicated Slots' }, ]; } @@ -457,7 +457,7 @@ export default function ProgressModal({ return `Mapping Content for ${match[1]} Image Prompts`; } } - return 'Mapping Content for X Image Prompts'; + return 'Mapping content for image prompts'; } else if (stepPhase === 'AI_CALL') { // For AI_CALL: Show "Writing Featured Image Prompts" return 'Writing Featured Image Prompts'; @@ -475,7 +475,7 @@ export default function ProgressModal({ return `Writing ${match[1]} In‑article Image Prompts`; } } - return 'Writing X In‑article Image Prompts'; + return 'Writing In‑article Image Prompts'; } else if (stepPhase === 'SAVE') { // For SAVE: Extract prompt count from message const promptCount = extractCount(/(\d+)\s+Prompts/i) || extractCount(/(\d+)\s+prompt/i); diff --git a/frontend/src/config/pages/clusters.config.tsx b/frontend/src/config/pages/clusters.config.tsx index 0dfaf5e7..e2859131 100644 --- a/frontend/src/config/pages/clusters.config.tsx +++ b/frontend/src/config/pages/clusters.config.tsx @@ -137,7 +137,17 @@ export const createClustersPageConfig = ( sortable: false, // Backend doesn't support sorting by ideas_count sortField: 'ideas_count', width: '120px', - render: (value: number) => value.toLocaleString(), + render: (value: number) => ( + 0 ? 'success' : 'light'} + size="xs" + variant="soft" + > + + {value > 0 ? `${value.toLocaleString()} ideas` : 'No ideas'} + + + ), }, { key: 'volume', diff --git a/frontend/src/config/pages/ideas.config.tsx b/frontend/src/config/pages/ideas.config.tsx index 9257338c..2a8cc2e2 100644 --- a/frontend/src/config/pages/ideas.config.tsx +++ b/frontend/src/config/pages/ideas.config.tsx @@ -4,6 +4,7 @@ */ import React from 'react'; +import { Link } from 'react-router-dom'; import { titleColumn, sectorColumn, @@ -164,7 +165,19 @@ export const createIdeasPageConfig = ( sortable: false, // Backend doesn't support sorting by keyword_cluster_id sortField: 'keyword_cluster_id', width: '200px', - render: (_value: string, row: ContentIdea) => row.keyword_cluster_name || '-', + render: (_value: string, row: ContentIdea) => { + if (row.keyword_cluster_id && row.keyword_cluster_name) { + return ( + + {row.keyword_cluster_name} + + ); + } + return '-'; + }, }, { ...statusColumn, diff --git a/frontend/src/pages/Planner/KeywordOpportunities.tsx b/frontend/src/pages/Planner/KeywordOpportunities.tsx deleted file mode 100644 index 417c3c12..00000000 --- a/frontend/src/pages/Planner/KeywordOpportunities.tsx +++ /dev/null @@ -1,680 +0,0 @@ -/** - * Keyword Opportunities Page - * Shows available SeedKeywords for the active site/sectors - * Allows users to add keywords to their workflow - */ - -import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; -import TablePageTemplate from '../../templates/TablePageTemplate'; -import { - fetchSeedKeywords, - SeedKeyword, - SeedKeywordResponse, - addSeedKeywordsToWorkflow, -} from '../../services/api'; -import { useSiteStore } from '../../store/siteStore'; -import { useSectorStore } from '../../store/sectorStore'; -import { usePageSizeStore } from '../../store/pageSizeStore'; -import { useToast } from '../../components/ui/toast/ToastContainer'; -import { getDifficultyLabelFromNumber, getDifficultyRange, getDifficultyNumber } from '../../utils/difficulty'; -import Badge from '../../components/ui/badge/Badge'; -import { formatRelativeDate } from '../../utils/date'; -import { BoltIcon, PlusIcon } from '../../icons'; -import PageHeader from '../../components/common/PageHeader'; - -export default function KeywordOpportunities() { - const toast = useToast(); - const { activeSite } = useSiteStore(); - const { activeSector, loadSectorsForSite } = useSectorStore(); - const { pageSize } = usePageSizeStore(); - - // Data state - const [seedKeywords, setSeedKeywords] = useState<(SeedKeyword & { isAdded?: boolean })[]>([]); - const [loading, setLoading] = useState(true); - const [showContent, setShowContent] = useState(false); - const [selectedIds, setSelectedIds] = useState([]); - // Track recently added keywords to preserve their state during reload - const recentlyAddedRef = useRef>(new Set()); - - // Pagination state - const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [totalCount, setTotalCount] = useState(0); - - // Sorting state - const [sortBy, setSortBy] = useState('keyword'); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); - - // Filter state - const [searchTerm, setSearchTerm] = useState(''); - const [countryFilter, setCountryFilter] = useState(''); - const [difficultyFilter, setDifficultyFilter] = useState(''); - const [volumeMin, setVolumeMin] = useState(''); - const [volumeMax, setVolumeMax] = useState(''); - - // Load sectors for active site - useEffect(() => { - if (activeSite?.id) { - loadSectorsForSite(activeSite.id); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeSite?.id]); // loadSectorsForSite is stable from Zustand store, no need to include it - - // Load seed keywords - const loadSeedKeywords = useCallback(async () => { - if (!activeSite || !activeSite.industry) { - setSeedKeywords([]); - setTotalCount(0); - setTotalPages(1); - setLoading(false); - return; - } - - setLoading(true); - setShowContent(false); - - try { - // Get already-attached keywords across ALL sectors for this site - let attachedSeedKeywordIds = new Set(); - try { - const { fetchKeywords, fetchSiteSectors } = await import('../../services/api'); - // Get all sectors for the site - const sectors = await fetchSiteSectors(activeSite.id); - - // Check keywords in all sectors - for (const sector of sectors) { - try { - const keywordsData = await fetchKeywords({ - site_id: activeSite.id, - sector_id: sector.id, - page_size: 1000, // Get all to check which are attached - }); - (keywordsData.results || []).forEach((k: any) => { - // seed_keyword_id is write_only in serializer, so use seed_keyword.id instead - const seedKeywordId = k.seed_keyword_id || (k.seed_keyword && k.seed_keyword.id); - if (seedKeywordId) { - attachedSeedKeywordIds.add(Number(seedKeywordId)); - } - }); - } catch (err) { - // If keywords fetch fails for a sector, continue with others - console.warn(`Could not fetch attached keywords for sector ${sector.id}:`, err); - } - } - } catch (err) { - // If sectors fetch fails, continue without filtering - console.warn('Could not fetch sectors or attached keywords:', err); - } - - // Build filters - fetch ALL results by paginating through all pages - const baseFilters: any = { - industry: activeSite.industry, - page_size: 1000, // Use reasonable page size (API might have max limit) - }; - - // Add sector filter if active sector is selected - // IMPORTANT: Filter by industry_sector (IndustrySector ID) which is what SeedKeyword.sector references - if (activeSector && activeSector.industry_sector) { - baseFilters.sector = activeSector.industry_sector; - } - - if (searchTerm) baseFilters.search = searchTerm; - if (countryFilter) baseFilters.country = countryFilter; - - // Fetch ALL pages to get complete dataset - let allResults: SeedKeyword[] = []; - let currentPageNum = 1; - let hasMore = true; - - while (hasMore) { - const filters = { ...baseFilters, page: currentPageNum }; - const data: SeedKeywordResponse = await fetchSeedKeywords(filters); - - if (data.results && data.results.length > 0) { - allResults = [...allResults, ...data.results]; - } - - // Check if there are more pages - hasMore = data.next !== null && data.next !== undefined; - currentPageNum++; - - // Safety limit to prevent infinite loops - if (currentPageNum > 100) { - console.warn('Reached maximum page limit (100) while fetching seed keywords'); - break; - } - } - - // Mark already-attached keywords instead of filtering them out - // Also check recentlyAddedRef to preserve state for keywords just added - let filteredResults = allResults.map(sk => { - const isAdded = attachedSeedKeywordIds.has(Number(sk.id)) || recentlyAddedRef.current.has(Number(sk.id)); - return { - ...sk, - isAdded: Boolean(isAdded) // Explicitly convert to boolean true/false - }; - }); - - if (difficultyFilter) { - const difficultyNum = parseInt(difficultyFilter); - const label = getDifficultyLabelFromNumber(difficultyNum); - if (label !== null) { - const range = getDifficultyRange(label); - if (range) { - filteredResults = filteredResults.filter( - sk => sk.difficulty >= range.min && sk.difficulty <= range.max - ); - } - } - } - - if (volumeMin !== '' && volumeMin !== null && volumeMin !== undefined) { - filteredResults = filteredResults.filter(sk => sk.volume >= Number(volumeMin)); - } - if (volumeMax !== '' && volumeMax !== null && volumeMax !== undefined) { - filteredResults = filteredResults.filter(sk => sk.volume <= Number(volumeMax)); - } - - // Apply client-side sorting - if (sortBy) { - filteredResults.sort((a, b) => { - let aVal: any; - let bVal: any; - - if (sortBy === 'keyword') { - aVal = a.keyword.toLowerCase(); - bVal = b.keyword.toLowerCase(); - } else if (sortBy === 'volume') { - aVal = a.volume; - bVal = b.volume; - } else if (sortBy === 'difficulty') { - aVal = a.difficulty; - bVal = b.difficulty; - } else if (sortBy === 'intent') { - aVal = a.intent.toLowerCase(); - bVal = b.intent.toLowerCase(); - } else { - return 0; - } - - if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; - if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; - return 0; - }); - } - - // Calculate total count and pages from filtered results - const totalFiltered = filteredResults.length; - const pageSizeNum = pageSize || 10; - - // Apply client-side pagination - const startIndex = (currentPage - 1) * pageSizeNum; - const endIndex = startIndex + pageSizeNum; - const paginatedResults = filteredResults.slice(startIndex, endIndex); - - - setSeedKeywords(paginatedResults); - setTotalCount(totalFiltered); - setTotalPages(Math.ceil(totalFiltered / pageSizeNum)); - - setShowContent(true); - } catch (error: any) { - console.error('Error loading seed keywords:', error); - toast.error(`Failed to load keyword opportunities: ${error.message}`); - setSeedKeywords([]); - setTotalCount(0); - setTotalPages(1); - } finally { - setLoading(false); - } - }, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, difficultyFilter, volumeMin, volumeMax, sortBy, sortDirection]); - - // Load data on mount and when filters change (excluding search - handled separately) - useEffect(() => { - loadSeedKeywords(); - }, [loadSeedKeywords]); - - // Debounced search - reset to page 1 when search term changes - useEffect(() => { - const timer = setTimeout(() => { - setCurrentPage(1); - }, 500); - - return () => clearTimeout(timer); - }, [searchTerm]); // Only depend on searchTerm - - // Handle pageSize changes - reload data when pageSize changes - // Note: loadSeedKeywords will be recreated when pageSize changes (it's in its dependencies) - // The effect that depends on loadSeedKeywords will handle the reload - // We just need to reset to page 1 - useEffect(() => { - setCurrentPage(1); - }, [pageSize]); // Only depend on pageSize - - // Handle sorting - const handleSort = (field: string, direction: 'asc' | 'desc') => { - setSortBy(field || 'keyword'); - setSortDirection(direction); - setCurrentPage(1); - }; - - // Handle adding keywords to workflow - const handleAddToWorkflow = useCallback(async (seedKeywordIds: number[]) => { - if (!activeSite) { - toast.error('Please select an active site first'); - return; - } - - // Get sector to use - use activeSector if available, otherwise get first available sector - let sectorToUse = activeSector; - if (!sectorToUse) { - try { - const { fetchSiteSectors } = await import('../../services/api'); - const sectors = await fetchSiteSectors(activeSite.id); - if (sectors.length === 0) { - toast.error('No sectors available for this site. Please create a sector first.'); - return; - } - sectorToUse = { - id: sectors[0].id, - name: sectors[0].name, - slug: sectors[0].slug, - site_id: activeSite.id, - is_active: sectors[0].is_active !== false, - industry_sector: sectors[0].industry_sector || null, - }; - } catch (error: any) { - toast.error(`Failed to get sectors: ${error.message}`); - return; - } - } - - try { - const result = await addSeedKeywordsToWorkflow( - seedKeywordIds, - activeSite.id, - sectorToUse.id - ); - - if (result.success) { - // Show success message with created count - if (result.created > 0) { - toast.success(`Successfully added ${result.created} keyword(s) to workflow`); - } - - // Show skipped count if any - if (result.skipped > 0) { - toast.warning(`${result.skipped} keyword(s) were skipped (already exist or validation failed)`); - } - - // Show detailed errors if any - if (result.errors && result.errors.length > 0) { - result.errors.forEach((error: string) => { - toast.error(error, { duration: 8000 }); - }); - } - - // Only track and mark as added if actually created - if (result.created > 0) { - // Track these as recently added to preserve state during reload - seedKeywordIds.forEach(id => { - recentlyAddedRef.current.add(id); - }); - - // Immediately update state to mark keywords as added - this gives instant feedback - setSeedKeywords(prevKeywords => - prevKeywords.map(kw => - seedKeywordIds.includes(kw.id) - ? { ...kw, isAdded: true } - : kw - ) - ); - } - - // Clear selection - setSelectedIds([]); - - // Don't reload immediately - the state is already updated - // The recentlyAddedRef will ensure they stay marked as added - // Only reload if user changes filters/pagination - } else { - toast.error(`Failed to add keywords: ${result.errors?.join(', ') || 'Unknown error'}`); - } - } catch (error: any) { - toast.error(`Failed to add keywords: ${error.message}`); - } - }, [activeSite, activeSector, toast]); - - // Handle bulk add selected - filter out already added keywords - const handleBulkAddSelected = useCallback(async (ids: string[]) => { - if (ids.length === 0) { - toast.error('Please select at least one keyword'); - return; - } - - // Filter out already added keywords - const availableIds = ids.filter(id => { - const keyword = seedKeywords.find(sk => String(sk.id) === id); - return keyword && !keyword.isAdded; - }); - - if (availableIds.length === 0) { - toast.error('All selected keywords are already added to workflow'); - return; - } - - if (availableIds.length < ids.length) { - toast.info(`${ids.length - availableIds.length} keyword(s) were already added and were skipped`); - } - - const seedKeywordIds = availableIds.map(id => parseInt(id)); - await handleAddToWorkflow(seedKeywordIds); - }, [handleAddToWorkflow, toast, seedKeywords]); - - // Handle add all - fetch all keywords for site/sectors, not just current page - const handleAddAll = useCallback(async () => { - if (!activeSite || !activeSite.industry) { - toast.error('Please select an active site first'); - return; - } - - try { - // Fetch ALL seed keywords for the site/sectors (no pagination) - const filters: any = { - industry: activeSite.industry, - page_size: 1000, // Large page size to get all - }; - - if (activeSector?.industry_sector) { - filters.sector = activeSector.industry_sector; - } - - const data: SeedKeywordResponse = await fetchSeedKeywords(filters); - const allSeedKeywords = data.results || []; - - if (allSeedKeywords.length === 0) { - toast.error('No keywords available to add'); - return; - } - - // Get already-added keywords to filter them out - const { fetchKeywords, fetchSiteSectors } = await import('../../services/api'); - const sectors = await fetchSiteSectors(activeSite.id); - let attachedSeedKeywordIds = new Set(); - - for (const sector of sectors) { - try { - const keywordsData = await fetchKeywords({ - site_id: activeSite.id, - sector_id: sector.id, - page_size: 1000, - }); - (keywordsData.results || []).forEach((k: any) => { - // seed_keyword_id is write_only in serializer, so use seed_keyword.id instead - const seedKeywordId = k.seed_keyword_id || (k.seed_keyword && k.seed_keyword.id); - if (seedKeywordId) { - attachedSeedKeywordIds.add(Number(seedKeywordId)); - } - }); - } catch (err) { - console.warn(`Could not fetch attached keywords for sector ${sector.id}:`, err); - } - } - - // Filter out already added keywords - const availableKeywords = allSeedKeywords.filter(sk => !attachedSeedKeywordIds.has(sk.id)); - - if (availableKeywords.length === 0) { - toast.error('All keywords are already added to workflow'); - return; - } - - if (availableKeywords.length < allSeedKeywords.length) { - toast.info(`${allSeedKeywords.length - availableKeywords.length} keyword(s) were already added and were skipped`); - } - - const seedKeywordIds = availableKeywords.map(sk => sk.id); - await handleAddToWorkflow(seedKeywordIds); - } catch (error: any) { - toast.error(`Failed to load all keywords: ${error.message}`); - } - }, [activeSite, activeSector, handleAddToWorkflow, toast]); - - // Page config - const pageConfig = useMemo(() => { - const showSectorColumn = !activeSector; // Show when viewing all sectors - - return { - columns: [ - { - key: 'keyword', - label: 'Keyword', - sortable: true, - sortField: 'keyword', - }, - ...(showSectorColumn ? [{ - key: 'sector_name', - label: 'Sector', - sortable: false, - render: (_value: string, row: SeedKeyword) => ( - - {row.sector_name || '-'} - - ), - }] : []), - { - key: 'volume', - label: 'Volume', - sortable: true, - sortField: 'volume', - render: (value: number) => value.toLocaleString(), - }, - { - key: 'difficulty', - label: 'Difficulty', - sortable: true, - sortField: 'difficulty', - align: 'center' as const, - render: (value: number) => { - const difficultyNum = getDifficultyNumber(value); - const difficultyBadgeVariant = 'light'; - const difficultyBadgeColor = - typeof difficultyNum === 'number' && difficultyNum === 1 - ? 'success' - : typeof difficultyNum === 'number' && difficultyNum === 2 - ? 'success' - : typeof difficultyNum === 'number' && difficultyNum === 3 - ? 'warning' - : typeof difficultyNum === 'number' && difficultyNum === 4 - ? 'error' - : typeof difficultyNum === 'number' && difficultyNum === 5 - ? 'error' - : 'light'; - return typeof difficultyNum === 'number' ? ( - - {difficultyNum} - - ) : ( - difficultyNum - ); - }, - }, - { - key: 'country', - label: 'Country', - sortable: true, - sortField: 'country', - render: (value: string) => { - const countryNames: Record = { - 'US': 'United States', - 'CA': 'Canada', - 'GB': 'United Kingdom', - 'AE': 'United Arab Emirates', - 'AU': 'Australia', - 'IN': 'India', - 'PK': 'Pakistan', - }; - return ( - - {value || '-'} - - ); - }, - }, - ], - filters: [ - { - key: 'search', - label: 'Search', - type: 'text', - placeholder: 'Search keywords...', - }, - { - key: 'country', - label: 'Country', - type: 'select', - options: [ - { value: '', label: 'All Countries' }, - { value: 'US', label: 'United States' }, - { value: 'CA', label: 'Canada' }, - { value: 'GB', label: 'United Kingdom' }, - { value: 'AE', label: 'United Arab Emirates' }, - { value: 'AU', label: 'Australia' }, - { value: 'IN', label: 'India' }, - { value: 'PK', label: 'Pakistan' }, - ], - }, - { - key: 'difficulty', - label: 'Difficulty', - type: 'select', - options: [ - { value: '', label: 'All Difficulty' }, - { value: '1', label: '1 - Very Easy' }, - { value: '2', label: '2 - Easy' }, - { value: '3', label: '3 - Medium' }, - { value: '4', label: '4 - Hard' }, - { value: '5', label: '5 - Very Hard' }, - ], - }, - ], - bulkActions: !activeSector ? [] : [ - { - key: 'add_selected_to_workflow', - label: 'Add Selected Keywords', - variant: 'primary' as const, - }, - ], - }; - }, [activeSector]); - - return ( - <> - , color: 'orange' }} - /> - - {/* Show info banner when no sector is selected */} - {!activeSector && activeSite && ( -
-
-
-
- - - -
-
-

- Choose a Topic Area First -

-

- Pick a topic area first, then add keywords - You need to choose what you're writing about before adding search terms to target -

-
-
-
-
- )} - - { - const stringValue = value === null || value === undefined ? '' : String(value); - - if (key === 'search') { - setSearchTerm(stringValue); - } else if (key === 'country') { - setCountryFilter(stringValue); - setCurrentPage(1); - } else if (key === 'difficulty') { - setDifficultyFilter(stringValue); - setCurrentPage(1); - } - }} - onRowAction={async (actionKey: string, row: SeedKeyword & { isAdded?: boolean }) => { - if (actionKey === 'add_to_workflow') { - // Check if sector is selected - if (!activeSector) { - toast.error('Please select a sector first'); - return; - } - // Don't allow adding already-added keywords - if (row.isAdded) { - toast.info('This keyword is already added to workflow'); - return; - } - await handleAddToWorkflow([row.id]); - } - }} - bulkActions={pageConfig.bulkActions} - onBulkAction={async (actionKey: string, ids: string[]) => { - if (actionKey === 'add_selected_to_workflow') { - if (!activeSector) { - toast.error('Please select a sector first'); - return; - } - await handleBulkAddSelected(ids); - } - }} - onCreate={activeSector ? handleAddAll : undefined} - createLabel="Add All to Workflow" - onCreateIcon={} - pagination={{ - currentPage, - totalPages, - totalCount, - onPageChange: setCurrentPage, - }} - sorting={{ - sortBy, - sortDirection, - onSort: handleSort, - }} - selection={{ - selectedIds, - onSelectionChange: setSelectedIds, - }} - /> - - ); -} - diff --git a/frontend/src/templates/ContentViewTemplate.tsx b/frontend/src/templates/ContentViewTemplate.tsx index 35126e74..fd862f0a 100644 --- a/frontend/src/templates/ContentViewTemplate.tsx +++ b/frontend/src/templates/ContentViewTemplate.tsx @@ -998,48 +998,6 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten - - {/* Tags and Categories from taxonomy_terms_data */} - {content.taxonomy_terms_data && content.taxonomy_terms_data.length > 0 ? ( -
-
- {content.taxonomy_terms_data.filter(term => term.taxonomy_type === 'tag').length > 0 && ( -
- -
- {content.taxonomy_terms_data - .filter(term => term.taxonomy_type === 'tag') - .map((tag) => ( - - {tag.name} - - ))} -
-
- )} - {content.taxonomy_terms_data.filter(term => term.taxonomy_type === 'category').length > 0 && ( -
- Categories: -
- {content.taxonomy_terms_data - .filter(term => term.taxonomy_type === 'category') - .map((category) => ( - - {category.name} - - ))} -
-
- )} -
-
- ) : null} {/* Action Buttons - Conditional based on status */} diff --git a/to-do-s/part1/SECTION_2_FINAL_MODS.md b/to-do-s/part1/SECTION_2_FINAL_MODS.md index c3c2ae5a..3f2e3c83 100644 --- a/to-do-s/part1/SECTION_2_FINAL_MODS.md +++ b/to-do-s/part1/SECTION_2_FINAL_MODS.md @@ -240,3 +240,50 @@ Site Setup Progress | Cleanup | Remove Site Builder and content fetching APIs/models | --- + +Status after implementation + + +## Summary of Section 2 Implementation + +### 2.1 Add Keywords (IndustriesSectorsKeywords.tsx) +- ✅ Added `showNotAddedOnly` filter state with "Not Yet Added Only" filter option +- ✅ Added `addedCount` and `availableCount` state variables for keyword count tracking +- ✅ Added keyword count summary showing "X keywords in your workflow • Y available to add" +- ✅ Added "Next: Plan Your Content →" CTA button that appears when keywords are added +- ✅ Added "Looking for more keywords? Keyword Research coming soon!" teaser text +- ✅ Sector requirement tooltip already existed - no changes needed +- ✅ No visible import buttons to remove (dead code existed but was not exposed) + +### 2.2 Content Settings (ContentSettingsPage.tsx) +- ✅ Created new backend API endpoint `/v1/system/settings/content//` for content_generation and publishing +- ✅ Added `ContentSettingsViewSet` to backend with retrieve/update/save actions +- ✅ Updated frontend to load content_generation and publishing settings from API +- ✅ Updated frontend to save content_generation settings (append_to_prompt, default_tone, default_length) +- ✅ Updated frontend to save publishing settings (auto_publish_enabled, auto_sync_enabled) +- ✅ Removed TODO comments - settings now actually persist + +### 2.3 Sites +- ✅ Created `SiteSetupChecklist` component showing setup progress with checklist +- ✅ Updated Dashboard.tsx to use the new checklist component +- ✅ Removed mock stats from Dashboard (were showing all zeros) +- ✅ Deleted Manage.tsx (redundant duplicate of List.tsx) +- ✅ Removed empty `Builder/` folder structure +- ✅ Removed routes to deleted pages in App.tsx +- ✅ Site Settings already defaults to "general" tab - no changes needed + +### Files Modified +1. IndustriesSectorsKeywords.tsx +2. ContentSettingsPage.tsx +3. settings_views.py +4. urls.py +5. Dashboard.tsx +6. App.tsx + +### Files Created +1. SiteSetupChecklist.tsx + +### Files Deleted +1. Manage.tsx +2. `frontend/src/pages/Sites/Builder/` (empty folder) + diff --git a/to-do-s/part1/SECTION_3_FINAL_MODS.md b/to-do-s/part1/SECTION_3_FINAL_MODS.md index 4d33f299..dde4d631 100644 --- a/to-do-s/part1/SECTION_3_FINAL_MODS.md +++ b/to-do-s/part1/SECTION_3_FINAL_MODS.md @@ -310,4 +310,42 @@ Not part of current phase. No issues to address. | Error recovery | API to retry failed items | | Notification logging | Ensure AI runs are logged for notification display | ---- \ No newline at end of file +--- +## After Implementation +Section 3 implementation is complete. Here's a summary of all changes made: + +## Section 3 WORKFLOW Modules - Implementation Complete ✅ + +### Completed Tasks: + +1. **Delete KeywordOpportunities.tsx** ✅ + - Deleted the orphaned page file + - Removed import and route from App.tsx + - Updated PAGES.md documentation + +2. **Add cluster ideas badge** ✅ + - Modified clusters.config.tsx + - Ideas column now shows badge: "X ideas" (green) or "No ideas" (gray) + +3. **Fix ContentView duplicate tags/categories** ✅ + - Modified ContentViewTemplate.tsx + - Removed duplicate tags/categories section that appeared below metadata + +4. **Fix progress modal placeholder texts** ✅ + - Modified ProgressModal.tsx + - Fixed "X" placeholders in step labels with cleaner fallbacks + - "Mapping Content for X Image Prompts" → "Mapping content for image prompts" + - "Writing X In‑article Image Prompts" → "Writing In‑article Image Prompts" + - Success message fallback cleaned up + +5. **Add queued count to Ideas** ✅ + - Already implemented via headerMetrics showing New/Queued/Completed counts + +6. **Clickable cluster in Ideas** ✅ + - Modified ideas.config.tsx + - Cluster name now links to `/planner/clusters/:id` + +### Documentation Updated: +- CHANGELOG.md - Added v1.1.5 section +- ENDPOINTS.md - Added Content Settings API docs +- PAGES.md - Removed KeywordOpportunities, updated version \ No newline at end of file