feat(search): add comprehensive keyword coverage and intelligent phrase matching

- Added 10+ new keyword categories (task, cluster, billing, invoice, payment, plan, usage, schedule, wordpress, writing, picture, user, ai)
- Implemented smart phrase normalization to strip filler words (how, to, what, is, etc.)
- Added duplicate prevention using Set to avoid showing same question multiple times
- Enhanced matching logic to check: direct keyword match, normalized term match, and question text match
- Supports basic stemming (plurals -> singular: tasks -> task)
- Now searches: 'how to import keywords' correctly matches 'import' in knowledge base
- Fixed duplicate keywords field in Team Management navigation item

This ensures all common search terms trigger relevant help suggestions with natural language support.
This commit is contained in:
IGNY8 VPS (Salman)
2026-01-09 16:37:34 +00:00
parent 264c720e3e
commit f04eb0a900
4 changed files with 1524 additions and 116 deletions

View File

@@ -0,0 +1,662 @@
# Phase 6: Data Backup & Cleanup Guide
**Version:** 1.0
**Created:** January 9, 2026
**Purpose:** Pre-V1.0 Launch Database Preparation
---
## 📋 Table of Contents
1. [Overview](#overview)
2. [What Was Created](#what-was-created)
3. [When to Use](#when-to-use)
4. [Pre-Execution Checklist](#pre-execution-checklist)
5. [Command 1: Export System Config](#command-1-export-system-config)
6. [Command 2: Cleanup User Data](#command-2-cleanup-user-data)
7. [Complete Workflow](#complete-workflow)
8. [Safety Measures](#safety-measures)
9. [Rollback Procedures](#rollback-procedures)
10. [FAQ](#faq)
---
## 📖 Overview
Phase 6 provides two Django management commands to safely prepare your IGNY8 database for V1.0 production launch:
1. **Export System Configuration** - Backs up all system settings to JSON files
2. **Cleanup User Data** - Removes all test/development user data while preserving system configuration
### Why These Commands?
- **Clean Start**: Launch V1.0 with a pristine database
- **Configuration Preservation**: Keep all your carefully configured settings
- **Safety First**: Multiple safety checks and dry-run options
- **Audit Trail**: Complete metadata and logging
---
## 🛠️ What Was Created
### File Locations
```
backend/igny8_core/management/commands/
├── export_system_config.py # System configuration backup
└── cleanup_user_data.py # User data cleanup
```
### Command 1: `export_system_config.py`
**Purpose**: Exports all system configuration to JSON files for backup and version control.
**What it exports:**
- ✅ Subscription Plans (Starter, Growth, Scale)
- ✅ Credit Cost Configurations
- ✅ AI Model Settings (OpenAI, Anthropic, etc.)
- ✅ Global Integration Settings
- ✅ Industries and Sectors
- ✅ Seed Keywords (reference data)
- ✅ Author Profiles
- ✅ AI Prompts and Variables
**What it creates:**
- Individual JSON files for each data type
- `export_metadata.json` with timestamp and statistics
- Organized folder structure in `backups/config/`
### Command 2: `cleanup_user_data.py`
**Purpose**: Safely removes all user-generated test data before production launch.
**What it deletes:**
- 🗑️ Sites and Site Settings
- 🗑️ Keywords, Clusters, Ideas
- 🗑️ Tasks, Content, Images
- 🗑️ Publishing Records
- 🗑️ WordPress Sync Events
- 🗑️ Credit Transactions and Usage Logs
- 🗑️ Automation Runs
- 🗑️ Notifications
- 🗑️ Orders
**What it preserves:**
- ✅ User Accounts (admin users)
- ✅ System Configuration (all settings from export)
- ✅ Plans and Pricing
- ✅ AI Models and Prompts
- ✅ Industries and Sectors
---
## ⏰ When to Use
### Correct Timing
**Use these commands when:**
- You're preparing for V1.0 production launch
- You've completed all testing and configuration
- You want to start production with clean data
- All system settings (Plans, AI models, prompts) are finalized
**Do NOT use these commands when:**
- You're still in active development
- You haven't backed up your configurations
- You're unsure about your system settings
- You're in production with live users
### Recommended Timeline
```
Day -7: Final configuration review
Day -5: Export system config (first backup)
Day -3: Test commands in staging
Day -2: Export system config (final backup)
Day -1: Cleanup user data in staging
Day 0: Launch day - cleanup in production
```
---
## ✅ Pre-Execution Checklist
Before running ANY Phase 6 command, complete this checklist:
### Environment Verification
- [ ] Confirm you're in the correct environment (staging vs production)
- [ ] Check `ENVIRONMENT` setting in Django settings
- [ ] Verify database connection is correct
- [ ] Ensure you have full database backup
### System State
- [ ] All Plans configured and tested
- [ ] All AI prompts finalized
- [ ] All credit costs verified
- [ ] All industries/sectors populated
- [ ] Seed keywords imported
### Safety Backups
- [ ] Full database dump exists
- [ ] Previous export exists (if available)
- [ ] Media files backed up
- [ ] Environment variables documented
### Access & Permissions
- [ ] You have Django shell access
- [ ] You have database backup access
- [ ] You have rollback permissions
- [ ] Stakeholders notified
---
## 📤 Command 1: Export System Config
### Basic Usage
```bash
cd /data/app/igny8/backend
python manage.py export_system_config
```
### With Custom Output Directory
```bash
python manage.py export_system_config --output-dir=/path/to/backup
```
### Step-by-Step Execution
#### Step 1: Navigate to Backend
```bash
cd /data/app/igny8/backend
```
#### Step 2: Run Export
```bash
python manage.py export_system_config --output-dir=../backups/config/$(date +%Y%m%d)
```
#### Step 3: Verify Output
```bash
ls -la ../backups/config/$(date +%Y%m%d)/
```
Expected output:
```
plans.json # Subscription plans
credit_costs.json # Credit cost configurations
ai_models.json # AI model settings
global_integrations.json # Integration settings
industries.json # Industry master data
sectors.json # Sector master data
seed_keywords.json # Reference keywords
author_profiles.json # Writing style profiles
prompts.json # AI prompts
prompt_variables.json # Prompt variables
export_metadata.json # Export timestamp & stats
```
#### Step 4: Verify Data
Check one of the exports:
```bash
cat ../backups/config/$(date +%Y%m%d)/plans.json | head -20
```
#### Step 5: Commit to Version Control
```bash
cd /data/app/igny8
git add backups/config/
git commit -m "Backup: V1.0 system configuration export"
git push
```
### What The Output Looks Like
```
Exporting system configuration to: /data/app/igny8/backups/config/20260109
✓ Exported 3 Subscription Plans → plans.json
✓ Exported 12 Credit Cost Configurations → credit_costs.json
✓ Exported 4 AI Model Configurations → ai_models.json
✓ Exported 1 Global Integration Settings → global_integrations.json
✓ Exported 15 Industries → industries.json
✓ Exported 47 Sectors → sectors.json
✓ Exported 523 Seed Keywords → seed_keywords.json
✓ Exported 3 Author Profiles → author_profiles.json
✓ Exported 8 AI Prompts → prompts.json
✓ Exported 12 Prompt Variables → prompt_variables.json
✓ Metadata saved to export_metadata.json
======================================================================
System Configuration Export Complete!
Successful: 10 exports
Failed: 0 exports
Location: /data/app/igny8/backups/config/20260109
======================================================================
```
### Troubleshooting
**Problem**: "No module named 'django'"
```bash
# Solution: Activate virtual environment or use Docker
docker-compose exec backend python manage.py export_system_config
```
**Problem**: "Permission denied" when writing files
```bash
# Solution: Check directory permissions
mkdir -p ../backups/config
chmod 755 ../backups/config
```
**Problem**: Empty JSON files
```bash
# Solution: Verify data exists in database
python manage.py shell
>>> from igny8_core.modules.billing.models import Plan
>>> Plan.objects.count()
```
---
## 🗑️ Command 2: Cleanup User Data
### ⚠️ CRITICAL WARNING
**THIS COMMAND PERMANENTLY DELETES DATA**
- Cannot be undone without database restore
- Removes ALL user-generated content
- Should ONLY be run before production launch
- ALWAYS run `--dry-run` first
### Safety Features
1. **Dry-Run Mode**: Preview deletions without actually deleting
2. **Confirmation Prompt**: Must type "DELETE ALL DATA" to proceed
3. **Production Protection**: Blocked in production environment (unless explicitly allowed)
4. **Transaction Safety**: All deletions in atomic transaction
5. **Detailed Logging**: Shows exactly what was deleted
### Usage: Dry Run (Always First!)
```bash
cd /data/app/igny8/backend
python manage.py cleanup_user_data --dry-run
```
### Dry Run Output Example
```
======================================================================
DRY RUN - No data will be deleted
======================================================================
✓ Would delete 1,234 Notifications
✓ Would delete 5,678 Credit Usage Logs
✓ Would delete 456 Credit Transactions
✓ Would delete 23 Orders
✓ Would delete 8,901 WordPress Sync Events
✓ Would delete 234 Publishing Records
✓ Would delete 45 Automation Runs
✓ Would delete 3,456 Images
✓ Would delete 2,345 Content
✓ Would delete 4,567 Tasks
✓ Would delete 5,678 Content Ideas
✓ Would delete 1,234 Clusters
✓ Would delete 9,876 Keywords
✓ Would delete 12 Sites
→ Keeping 3 Users (not deleted)
Total records to delete: 43,739
======================================================================
To proceed with actual deletion, run:
python manage.py cleanup_user_data --confirm
======================================================================
```
### Usage: Actual Cleanup
```bash
python manage.py cleanup_user_data --confirm
```
**You will be prompted:**
```
======================================================================
⚠️ DELETING ALL USER DATA - THIS CANNOT BE UNDONE!
======================================================================
Type "DELETE ALL DATA" to proceed:
```
**Type exactly:** `DELETE ALL DATA`
### Actual Cleanup Output
```
Proceeding with deletion...
✓ Deleted 1,234 Notifications
✓ Deleted 5,678 Credit Usage Logs
✓ Deleted 456 Credit Transactions
✓ Deleted 23 Orders
✓ Deleted 8,901 WordPress Sync Events
✓ Deleted 234 Publishing Records
✓ Deleted 45 Automation Runs
✓ Deleted 3,456 Images
✓ Deleted 2,345 Content
✓ Deleted 4,567 Tasks
✓ Deleted 5,678 Content Ideas
✓ Deleted 1,234 Clusters
✓ Deleted 9,876 Keywords
✓ Deleted 12 Sites
======================================================================
User Data Cleanup Complete!
Total records deleted: 43,739
Failed deletions: 0
======================================================================
```
### Production Environment Protection
If you try to run cleanup in production:
```
⚠️ BLOCKED: Cannot run cleanup in PRODUCTION environment!
To allow this, temporarily set ENVIRONMENT to "staging" in settings.
```
To override (ONLY if absolutely necessary):
```python
# In settings.py - TEMPORARY
ENVIRONMENT = 'staging' # Change back after cleanup!
```
---
## 🔄 Complete Workflow
### Full Pre-Launch Procedure
```bash
# ========================================
# STEP 1: FULL DATABASE BACKUP
# ========================================
cd /data/app/igny8/backend
pg_dump -h localhost -U postgres igny8_db > ../backups/$(date +%Y%m%d)_pre_v1_full_backup.sql
# Verify backup exists and has content
ls -lh ../backups/$(date +%Y%m%d)_pre_v1_full_backup.sql
head -50 ../backups/$(date +%Y%m%d)_pre_v1_full_backup.sql
# ========================================
# STEP 2: EXPORT SYSTEM CONFIGURATION
# ========================================
python manage.py export_system_config --output-dir=../backups/config/$(date +%Y%m%d)
# Verify exports
ls -la ../backups/config/$(date +%Y%m%d)/
# Review critical configs
cat ../backups/config/$(date +%Y%m%d)/plans.json | python -m json.tool | head -30
cat ../backups/config/$(date +%Y%m%d)/credit_costs.json | python -m json.tool | head -30
# ========================================
# STEP 3: COMMIT CONFIGS TO GIT
# ========================================
cd /data/app/igny8
git add backups/config/
git commit -m "Pre-V1.0: System configuration backup $(date +%Y%m%d)"
git push
# ========================================
# STEP 4: BACKUP MEDIA FILES
# ========================================
cd /data/app/igny8
tar -czf backups/$(date +%Y%m%d)_media_backup.tar.gz backend/media/
# ========================================
# STEP 5: DRY RUN CLEANUP (REVIEW CAREFULLY)
# ========================================
cd backend
python manage.py cleanup_user_data --dry-run
# Review the counts - make sure they're expected
# ========================================
# STEP 6: ACTUAL CLEANUP (POINT OF NO RETURN)
# ========================================
python manage.py cleanup_user_data --confirm
# Type: DELETE ALL DATA
# ========================================
# STEP 7: VERIFY CLEANUP
# ========================================
python manage.py shell << 'EOF'
from igny8_core.auth.models import Site, CustomUser
from igny8_core.business.planning.models import Keywords
from igny8_core.modules.billing.models import Plan
print(f"Sites: {Site.objects.count()} (should be 0)")
print(f"Keywords: {Keywords.objects.count()} (should be 0)")
print(f"Users: {CustomUser.objects.count()} (admins preserved)")
print(f"Plans: {Plan.objects.count()} (should have your plans)")
EOF
# ========================================
# STEP 8: TEST APPLICATION
# ========================================
python manage.py runserver 0.0.0.0:8000 &
# Visit app and verify:
# - Can login as admin
# - Dashboard loads (empty state)
# - Plans visible in settings
# - Can create new user account
# ========================================
# STEP 9: TAG RELEASE
# ========================================
cd /data/app/igny8
git tag -a v1.0.0-clean -m "V1.0.0 - Clean database ready for launch"
git push origin v1.0.0-clean
```
---
## 🛡️ Safety Measures
### Built-in Protections
1. **Atomic Transactions**: All deletions in single transaction - all or nothing
2. **Production Check**: Requires explicit override in production
3. **Confirmation Prompt**: Must type exact phrase
4. **Dry Run**: See exactly what will be deleted
5. **Detailed Logging**: Know what was deleted and any failures
### Manual Safety Checklist
Before running cleanup:
- [ ] **Full database backup** exists and verified
- [ ] **System config export** completed and committed to git
- [ ] **Media files** backed up
- [ ] **Dry run reviewed** and counts are expected
- [ ] **Stakeholders notified** of pending cleanup
- [ ] **Rollback plan** documented and tested
- [ ] **Off-hours execution** scheduled (if production)
- [ ] **Monitoring ready** to catch any issues
### Additional Recommendations
1. **Staging First**: Always test in staging environment first
2. **Screenshot Evidence**: Take screenshots of dry-run output
3. **Communication**: Notify team before and after
4. **Timing**: Run during low-traffic hours
5. **Verification**: Test application immediately after
---
## 🔙 Rollback Procedures
### If Something Goes Wrong
#### During Cleanup (Transaction Failed)
No action needed - atomic transaction will automatically rollback.
#### After Cleanup (Need to Restore)
```bash
# OPTION 1: Restore from PostgreSQL backup
cd /data/app/igny8
psql -U postgres -d igny8_db < backups/20260109_pre_v1_full_backup.sql
# OPTION 2: Restore specific tables (if partial restore needed)
pg_restore -U postgres -d igny8_db -t "specific_table" backups/20260109_pre_v1_full_backup.sql
# OPTION 3: Restore from Docker backup (if using Docker)
docker-compose exec -T db psql -U postgres igny8_db < backups/20260109_pre_v1_full_backup.sql
```
#### Restore Media Files
```bash
cd /data/app/igny8
tar -xzf backups/20260109_media_backup.tar.gz -C backend/
```
#### Verify Restore
```bash
cd backend
python manage.py shell << 'EOF'
from igny8_core.auth.models import Site
from igny8_core.business.planning.models import Keywords
print(f"Sites restored: {Site.objects.count()}")
print(f"Keywords restored: {Keywords.objects.count()}")
EOF
```
---
## ❓ FAQ
### Q: Can I run these commands multiple times?
**A:**
- **Export Config**: Yes, safe to run multiple times. Creates timestamped backups.
- **Cleanup**: Yes, but after first cleanup there's nothing left to delete (idempotent).
### Q: What if I only want to delete some data?
**A:** These commands are all-or-nothing by design for safety. To delete specific data, use Django admin or write a custom management command.
### Q: Can I restore individual items from the export?
**A:** Yes! The JSON files use Django's standard serialization format. Use `python manage.py loaddata <file>.json` to restore.
### Q: Will this affect my development environment?
**A:** Only if you run it there. These commands work on whatever database your Django settings point to.
### Q: How long does cleanup take?
**A:** Depends on data volume. Typical ranges:
- Small (< 10k records): 1-5 seconds
- Medium (10k-100k): 5-30 seconds
- Large (> 100k): 30-120 seconds
### Q: What if cleanup fails halfway?
**A:** Can't happen - it's wrapped in an atomic transaction. Either everything deletes or nothing does.
### Q: Do I need to stop the application?
**A:** Recommended but not required. Stopping the app prevents race conditions during cleanup.
### Q: Can I schedule these as cron jobs?
**A:**
- **Export**: Yes, great for automated backups
- **Cleanup**: No, should only be run manually with explicit confirmation
### Q: What about Django migrations?
**A:** Cleanup only deletes data, not schema. All tables and migrations remain intact.
### Q: How do I know if my system config is complete?
**A:** Run the export and review the counts in `export_metadata.json`. Compare with your documentation.
---
## 📞 Support
### If You Need Help
1. **Check this guide** thoroughly first
2. **Review error messages** carefully
3. **Test in staging** before production
4. **Contact team** if unsure about any step
### Emergency Contacts
- **Database Issues**: DBA team
- **Application Issues**: Backend team
- **Configuration Questions**: System admin
- **Rollback Needed**: All hands on deck!
---
## ✅ Success Criteria
After completing Phase 6, you should have:
- ✅ Multiple timestamped config exports in `backups/config/`
- ✅ Full database SQL backup in `backups/`
- ✅ Media files backup in `backups/`
- ✅ Zero user-generated data in database
- ✅ All system configurations intact
- ✅ Application starts and loads empty state
- ✅ Admin can log in
- ✅ New users can sign up
- ✅ Plans visible and functional
- ✅ Git tag created for v1.0.0-clean
---
**Document Version:** 1.0
**Last Updated:** January 9, 2026
**Next Review:** After V1.0 Launch
---
*This guide is part of the IGNY8 Pre-Launch Preparation (Phase 6)*

View File

@@ -7,17 +7,60 @@ import { useNavigate } from 'react-router-dom';
import { Modal } from '../ui/modal';
import Button from '../ui/button/Button';
// Add styles for highlighted search terms
const searchHighlightStyles = `
.search-result mark {
background-color: rgb(252 211 77); /* amber-300 */
color: rgb(17 24 39); /* gray-900 */
padding: 0 0.25rem;
border-radius: 0.25rem;
font-weight: 500;
transition: all 0.2s;
}
.dark .search-result mark {
background-color: rgb(180 83 9); /* amber-700 */
color: rgb(255 255 255);
}
.search-result:hover mark {
background-color: rgb(245 158 11); /* amber-500 */
color: rgb(255 255 255);
box-shadow: 0 0 0 2px rgb(245 158 11 / 0.3);
}
.dark .search-result:hover mark {
background-color: rgb(217 119 6); /* amber-600 */
box-shadow: 0 0 0 2px rgb(217 119 6 / 0.3);
}
`;
interface SearchModalProps {
isOpen: boolean;
onClose: () => void;
}
interface QuickAction {
label: string;
path?: string;
action?: () => void;
}
interface SearchResult {
title: string;
path: string;
type: 'workflow' | 'setup' | 'account' | 'help';
category: string;
description?: string;
icon?: string;
quickActions?: QuickAction[];
keywords?: string[]; // Additional searchable terms
content?: string; // Page content hints for better search
contextSnippet?: string; // Context around matched text
}
interface SuggestedQuestion {
question: string;
answer: string;
helpSection: string;
path: string;
}
type FilterType = 'all' | 'workflow' | 'setup' | 'account' | 'help';
@@ -25,32 +68,378 @@ type FilterType = 'all' | 'workflow' | 'setup' | 'account' | 'help';
const RECENT_SEARCHES_KEY = 'igny8_recent_searches';
const MAX_RECENT_SEARCHES = 5;
// Knowledge base for suggested questions and answers
// Keys include main terms + common aliases for better search matching
const HELP_KNOWLEDGE_BASE: Record<string, SuggestedQuestion[]> = {
'keyword': [
{ question: 'How do I import keywords?', answer: 'Go to Add Keywords page and either select your industry/sector for seed keywords or upload a CSV file with your own keywords.', helpSection: 'Importing Keywords', path: '/help#importing-keywords' },
{ question: 'How do I organize keywords into clusters?', answer: 'Navigate to Clusters page and run the AI clustering algorithm. It will automatically group similar keywords by topic.', helpSection: 'Keyword Clustering', path: '/help#keyword-clustering' },
{ question: 'Can I bulk delete keywords?', answer: 'Yes, on the Keywords page select multiple keywords using checkboxes and click the bulk delete action button.', helpSection: 'Managing Keywords', path: '/help#managing-keywords' },
],
'cluster': [ // Added alias for clustering
{ question: 'How do I organize keywords into clusters?', answer: 'Navigate to Clusters page and run the AI clustering algorithm. It will automatically group similar keywords by topic.', helpSection: 'Keyword Clustering', path: '/help#keyword-clustering' },
{ question: 'Can I bulk delete keywords?', answer: 'Yes, on the Keywords page select multiple keywords using checkboxes and click the bulk delete action button.', helpSection: 'Managing Keywords', path: '/help#managing-keywords' },
],
'task': [ // Added for tasks
{ question: 'How do I generate content?', answer: 'Convert content ideas to tasks in the Queue, or create tasks manually. The AI will generate content based on your keywords and settings.', helpSection: 'Content Generation', path: '/help#content-generation' },
{ question: 'What is the difference between Tasks and Content?', answer: 'Tasks are content ideas converted into actionable writing assignments with status tracking. Content is the actual generated articles created from tasks.', helpSection: 'Content Workflow', path: '/help#content-workflow' },
],
'content': [
{ question: 'How do I generate content?', answer: 'Convert content ideas to tasks in the Queue, or create tasks manually. The AI will generate content based on your keywords and settings.', helpSection: 'Content Generation', path: '/help#content-generation' },
{ question: 'How do I edit generated content?', answer: 'Go to Drafts page, click on any content to open the editor. You can edit text, title, and metadata before approving.', helpSection: 'Editing Content', path: '/help#editing-content' },
{ question: 'What content settings can I configure?', answer: 'In Content Settings you can set default length, tone, style, SEO preferences, and image generation settings.', helpSection: 'Content Settings', path: '/help#content-settings' },
{ question: 'How do I approve content for publishing?', answer: 'Review content in the Review page, then click approve to move it to the Approved queue ready for publishing.', helpSection: 'Content Workflow', path: '/help#content-workflow' },
],
'writing': [ // Added alias
{ question: 'How do I generate content?', answer: 'Convert content ideas to tasks in the Queue, or create tasks manually. The AI will generate content based on your keywords and settings.', helpSection: 'Content Generation', path: '/help#content-generation' },
{ question: 'How do I edit generated content?', answer: 'Go to Drafts page, click on any content to open the editor. You can edit text, title, and metadata before approving.', helpSection: 'Editing Content', path: '/help#editing-content' },
],
'publish': [
{ question: 'How do I publish to WordPress?', answer: 'Connect your WordPress site in Sites page, then use Content Calendar to schedule or immediately publish approved content.', helpSection: 'Publishing', path: '/help#publishing-wordpress' },
{ question: 'Can I schedule posts in advance?', answer: 'Yes, in the Content Calendar you can drag and drop content to specific dates and times for automatic publishing.', helpSection: 'Scheduling', path: '/help#scheduling-posts' },
{ question: 'How do I connect a WordPress site?', answer: 'Go to Sites page, click Add Site, enter your WordPress URL and credentials. Test the connection before saving.', helpSection: 'WordPress Integration', path: '/help#wordpress-integration' },
],
'wordpress': [ // Added alias
{ question: 'How do I publish to WordPress?', answer: 'Connect your WordPress site in Sites page, then use Content Calendar to schedule or immediately publish approved content.', helpSection: 'Publishing', path: '/help#publishing-wordpress' },
{ question: 'How do I connect a WordPress site?', answer: 'Go to Sites page, click Add Site, enter your WordPress URL and credentials. Test the connection before saving.', helpSection: 'WordPress Integration', path: '/help#wordpress-integration' },
],
'schedule': [ // Added alias
{ question: 'Can I schedule posts in advance?', answer: 'Yes, in the Content Calendar you can drag and drop content to specific dates and times for automatic publishing.', helpSection: 'Scheduling', path: '/help#scheduling-posts' },
],
'image': [
{ question: 'How do I generate images?', answer: 'Images are auto-generated with content. You can also regenerate specific images from the Images page with custom prompts.', helpSection: 'Image Generation', path: '/help#image-generation' },
{ question: 'Can I use different AI image models?', answer: 'Yes, configure your preferred AI image model (DALL-E, Midjourney, Stable Diffusion) in Content Settings under Images.', helpSection: 'Image Settings', path: '/help#image-settings' },
{ question: 'How do I assign images to content?', answer: 'From the Images page, click on an image and select which content to assign it as featured image.', helpSection: 'Managing Images', path: '/help#managing-images' },
],
'picture': [ // Added alias
{ question: 'How do I generate images?', answer: 'Images are auto-generated with content. You can also regenerate specific images from the Images page with custom prompts.', helpSection: 'Image Generation', path: '/help#image-generation' },
],
'credit': [
{ question: 'How do credits work?', answer: 'Credits are consumed for AI operations: keyword clustering, content generation, and image creation. Check Usage Analytics for detailed breakdown.', helpSection: 'Credit System', path: '/help#credit-system' },
{ question: 'How do I buy more credits?', answer: 'Go to Plans & Billing page to purchase credit packs or upgrade your subscription plan for more monthly credits.', helpSection: 'Purchasing Credits', path: '/help#purchasing-credits' },
{ question: 'Where can I see credit usage?', answer: 'Usage Analytics page shows detailed charts and logs of credit consumption by action type and date.', helpSection: 'Usage Tracking', path: '/help#usage-tracking' },
],
'billing': [ // Added for billing
{ question: 'How do I buy more credits?', answer: 'Go to Plans & Billing page to purchase credit packs or upgrade your subscription plan for more monthly credits.', helpSection: 'Purchasing Credits', path: '/help#purchasing-credits' },
{ question: 'What payment methods are supported?', answer: 'IGNY8 supports Stripe (credit/debit cards), PayPal, and Bank Transfer (for annual plans). Available methods vary by country.', helpSection: 'Purchasing Credits', path: '/help#purchasing-credits' },
],
'payment': [ // Added alias
{ question: 'How do I buy more credits?', answer: 'Go to Plans & Billing page to purchase credit packs or upgrade your subscription plan for more monthly credits.', helpSection: 'Purchasing Credits', path: '/help#purchasing-credits' },
{ question: 'What payment methods are supported?', answer: 'IGNY8 supports Stripe (credit/debit cards), PayPal, and Bank Transfer (for annual plans). Available methods vary by country.', helpSection: 'Purchasing Credits', path: '/help#purchasing-credits' },
],
'invoice': [ // Added for invoice
{ question: 'How do I buy more credits?', answer: 'Go to Plans & Billing page to purchase credit packs or upgrade your subscription plan for more monthly credits.', helpSection: 'Purchasing Credits', path: '/help#purchasing-credits' },
{ question: 'Where can I see billing history?', answer: 'Go to Plans & Billing page to view your invoices, payment history, and download receipts for your records.', helpSection: 'Purchasing Credits', path: '/help#purchasing-credits' },
],
'plan': [ // Added for subscription plans
{ question: 'How do I buy more credits?', answer: 'Go to Plans & Billing page to purchase credit packs or upgrade your subscription plan for more monthly credits.', helpSection: 'Purchasing Credits', path: '/help#purchasing-credits' },
{ question: 'Can I upgrade my plan?', answer: 'Yes, go to Plans & Billing to upgrade or downgrade your subscription. Changes take effect immediately with prorated billing.', helpSection: 'Purchasing Credits', path: '/help#purchasing-credits' },
],
'usage': [ // Added for usage
{ question: 'Where can I see credit usage?', answer: 'Usage Analytics page shows detailed charts and logs of credit consumption by action type and date.', helpSection: 'Usage Tracking', path: '/help#usage-tracking' },
{ question: 'How do credits work?', answer: 'Credits are consumed for AI operations: keyword clustering, content generation, and image creation. Check Usage Analytics for detailed breakdown.', helpSection: 'Credit System', path: '/help#credit-system' },
],
'automation': [
{ question: 'How do I set up automation?', answer: 'Go to Automation page to configure recurring tasks: auto-clustering, scheduled content generation, and auto-publishing rules.', helpSection: 'Automation Setup', path: '/help#automation-setup' },
{ question: 'Can content be auto-published?', answer: 'Yes, enable auto-approval rules in Automation and set publishing schedules in Content Calendar for fully automated workflows.', helpSection: 'Auto-Publishing', path: '/help#auto-publishing' },
],
'team': [
{ question: 'How do I invite team members?', answer: 'Go to Team Management, click Invite User, enter their email and assign a role. They will receive an invitation email.', helpSection: 'Team Collaboration', path: '/help#team-collaboration' },
{ question: 'What are the different user roles?', answer: 'Admin has full access, Editor can manage content, and Viewer can only view data. Configure in Team Management.', helpSection: 'User Roles', path: '/help#user-roles' },
],
'user': [ // Added alias
{ question: 'How do I invite team members?', answer: 'Go to Team Management, click Invite User, enter their email and assign a role. They will receive an invitation email.', helpSection: 'Team Collaboration', path: '/help#team-collaboration' },
{ question: 'What are the different user roles?', answer: 'Admin has full access, Editor can manage content, and Viewer can only view data. Configure in Team Management.', helpSection: 'User Roles', path: '/help#user-roles' },
],
'prompt': [
{ question: 'How do I customize AI prompts?', answer: 'Admins can edit AI prompt templates in Prompts page to control how content is generated.', helpSection: 'Prompt Management', path: '/help#prompt-management' },
{ question: 'What are author profiles?', answer: 'Author profiles define writing styles (tone, vocabulary, structure) that you can assign to content for consistent brand voice.', helpSection: 'Author Profiles', path: '/help#author-profiles' },
],
'ai': [ // Added alias
{ question: 'How do I customize AI prompts?', answer: 'Admins can edit AI prompt templates in Prompts page to control how content is generated.', helpSection: 'Prompt Management', path: '/help#prompt-management' },
{ question: 'Can I use different AI image models?', answer: 'Yes, configure your preferred AI image model (DALL-E, Midjourney, Stable Diffusion) in Content Settings under Images.', helpSection: 'Image Settings', path: '/help#image-settings' },
],
};
const SEARCH_ITEMS: SearchResult[] = [
// Workflow
{ title: 'Keywords', path: '/planner/keywords', type: 'workflow', category: 'Planner' },
{ title: 'Clusters', path: '/planner/clusters', type: 'workflow', category: 'Planner' },
{ title: 'Ideas', path: '/planner/ideas', type: 'workflow', category: 'Planner' },
{ title: 'Queue', path: '/writer/tasks', type: 'workflow', category: 'Writer' },
{ title: 'Drafts', path: '/writer/content', type: 'workflow', category: 'Writer' },
{ title: 'Images', path: '/writer/images', type: 'workflow', category: 'Writer' },
{ title: 'Review', path: '/writer/review', type: 'workflow', category: 'Writer' },
{ title: 'Approved', path: '/writer/approved', type: 'workflow', category: 'Writer' },
{ title: 'Automation', path: '/automation', type: 'workflow', category: 'Automation' },
{ title: 'Content Calendar', path: '/publisher/content-calendar', type: 'workflow', category: 'Publisher' },
// Workflow - Planner
{
title: 'Keywords',
path: '/planner/keywords',
type: 'workflow',
category: 'Planner',
description: 'Manage and organize your keywords',
keywords: ['keyword', 'search terms', 'seo', 'target', 'focus', 'research', 'phrases', 'queries'],
content: 'View and manage all your target keywords. Filter by cluster, search volume, or status. Bulk actions: delete, assign to cluster, export to CSV. Table shows keyword text, search volume, cluster assignment, and status.',
quickActions: [
{ label: 'Import Keywords', path: '/setup/add-keywords' },
{ label: 'View Clusters', path: '/planner/clusters' },
]
},
{
title: 'Clusters',
path: '/planner/clusters',
type: 'workflow',
category: 'Planner',
description: 'AI-grouped keyword clusters',
keywords: ['cluster', 'groups', 'topics', 'themes', 'organize', 'categorize', 'ai grouping'],
content: 'View AI-generated keyword clusters grouped by topic similarity. Each cluster shows assigned keywords count and suggested content topics. Run clustering algorithm, view cluster details, generate content ideas from clusters.',
quickActions: [
{ label: 'Generate Ideas', path: '/planner/ideas' },
{ label: 'View Keywords', path: '/planner/keywords' },
]
},
{
title: 'Ideas',
path: '/planner/ideas',
type: 'workflow',
category: 'Planner',
description: 'Content ideas from clusters',
keywords: ['ideas', 'suggestions', 'topics', 'content planning', 'brainstorm', 'article ideas'],
content: 'AI-generated content ideas based on keyword clusters. Review suggested titles, topics, and angles. Convert ideas to writing tasks with one click. Filter by cluster, status, or keyword.',
quickActions: [
{ label: 'Convert to Tasks', path: '/writer/tasks' },
{ label: 'View Clusters', path: '/planner/clusters' },
]
},
// Workflow - Writer
{
title: 'Queue',
path: '/writer/tasks',
type: 'workflow',
category: 'Writer',
description: 'Content generation queue',
keywords: ['queue', 'tasks', 'writing', 'generation', 'pending', 'in progress', 'batch', 'jobs'],
content: 'Content generation task queue. View pending, in-progress, and completed tasks. Monitor AI writing progress, cancel tasks, regenerate content. Shows task title, status, keywords, and generation progress.',
quickActions: [
{ label: 'View Drafts', path: '/writer/content' },
{ label: 'Check Images', path: '/writer/images' },
]
},
{
title: 'Drafts',
path: '/writer/content',
type: 'workflow',
category: 'Writer',
description: 'Generated content drafts',
keywords: ['drafts', 'content', 'articles', 'posts', 'generated', 'ai writing', 'edit', 'review'],
content: 'All AI-generated content drafts. Edit content in rich text editor, adjust title and metadata, assign featured images. Filter by keyword, status, or generation date. Bulk approve or delete drafts.',
quickActions: [
{ label: 'Move to Review', path: '/writer/review' },
{ label: 'View Images', path: '/writer/images' },
]
},
{
title: 'Images',
path: '/writer/images',
type: 'workflow',
category: 'Writer',
description: 'AI-generated images',
keywords: ['images', 'pictures', 'graphics', 'featured image', 'midjourney', 'dall-e', 'stable diffusion', 'ai art'],
content: 'AI-generated images library. View all generated images with prompts, assign to content, regenerate images. Filter by status, generation date, or content assignment. Supports multiple AI image models.',
quickActions: [
{ label: 'View Content', path: '/writer/content' },
{ label: 'Image Settings', path: '/account/content-settings/images' },
]
},
{
title: 'Review',
path: '/writer/review',
type: 'workflow',
category: 'Writer',
description: 'Content pending review',
keywords: ['review', 'approve', 'quality check', 'editorial', 'pending approval'],
content: 'Review AI-generated content before publishing. Check quality, accuracy, and brand alignment. Approve for publishing or send back to drafts for revisions.',
quickActions: [
{ label: 'Approve Content', path: '/writer/approved' },
{ label: 'View Drafts', path: '/writer/content' },
]
},
{
title: 'Approved',
path: '/writer/approved',
type: 'workflow',
category: 'Writer',
description: 'Ready to publish',
keywords: ['approved', 'ready', 'final', 'publish ready', 'scheduled'],
content: 'Approved content ready for publishing. Schedule for auto-publish or manually publish to WordPress sites. View publishing status and scheduled dates.',
quickActions: [
{ label: 'Schedule Publishing', path: '/publisher/content-calendar' },
{ label: 'View Sites', path: '/sites' },
]
},
// Workflow - Automation
{
title: 'Automation',
path: '/automation',
type: 'workflow',
category: 'Automation',
description: 'Pipeline automation settings',
keywords: ['automation', 'pipeline', 'workflow', 'auto', 'schedule', 'recurring', 'batch processing'],
content: 'Configure automated content pipeline. Set up recurring keyword clustering, content generation schedules, auto-approval rules, and publishing automation. Monitor automation runs and logs.',
quickActions: [
{ label: 'View Keywords', path: '/planner/keywords' },
{ label: 'Check Queue', path: '/writer/tasks' },
]
},
// Workflow - Publisher
{
title: 'Content Calendar',
path: '/publisher/content-calendar',
type: 'workflow',
category: 'Publisher',
description: 'Schedule and publish content',
keywords: ['calendar', 'schedule', 'publish', 'wordpress', 'posting', 'timeline', 'planning'],
content: 'Visual content calendar showing scheduled posts. Drag-and-drop to reschedule, bulk publish, view publishing history. Connect to WordPress sites for direct publishing.',
quickActions: [
{ label: 'View Approved', path: '/writer/approved' },
{ label: 'Manage Sites', path: '/sites' },
]
},
// Setup
{ title: 'Sites', path: '/sites', type: 'setup', category: 'Sites' },
{ title: 'Add Keywords', path: '/setup/add-keywords', type: 'setup', category: 'Setup' },
{ title: 'Content Settings', path: '/account/content-settings', type: 'setup', category: 'Settings' },
{ title: 'Prompts', path: '/thinker/prompts', type: 'setup', category: 'AI' },
{ title: 'Author Profiles', path: '/thinker/author-profiles', type: 'setup', category: 'AI' },
{
title: 'Sites',
path: '/sites',
type: 'setup',
category: 'Sites',
description: 'WordPress site management',
keywords: ['sites', 'wordpress', 'blog', 'website', 'connection', 'integration', 'wp', 'domain'],
content: 'Manage WordPress site connections. Add new sites, configure API credentials, test connections. View site details, publishing settings, and connection status. Supports multiple WordPress sites.',
quickActions: [
{ label: 'Add Keywords', path: '/setup/add-keywords' },
{ label: 'Content Settings', path: '/account/content-settings' },
]
},
{
title: 'Add Keywords',
path: '/setup/add-keywords',
type: 'setup',
category: 'Setup',
description: 'Import keywords by industry/sector',
keywords: ['import', 'add', 'bulk upload', 'csv', 'industry', 'sector', 'seed keywords', 'niche'],
content: 'Quick-start keyword import wizard. Select your industry and sector to import pre-researched seed keywords. Or upload your own CSV file with custom keywords. Bulk import thousands of keywords at once.',
quickActions: [
{ label: 'View Keywords', path: '/planner/keywords' },
{ label: 'Run Clustering', path: '/planner/clusters' },
]
},
{
title: 'Content Settings',
path: '/account/content-settings',
type: 'setup',
category: 'Settings',
description: 'Configure content generation',
keywords: ['settings', 'configuration', 'content length', 'tone', 'style', 'formatting', 'seo', 'meta'],
content: 'Configure AI content generation settings. Set default content length, tone of voice, writing style, SEO settings. Configure image generation, meta descriptions, and content structure preferences.',
quickActions: [
{ label: 'Edit Prompts', path: '/thinker/prompts' },
{ label: 'Author Profiles', path: '/thinker/author-profiles' },
]
},
{
title: 'Prompts',
path: '/thinker/prompts',
type: 'setup',
category: 'AI',
description: 'AI prompt templates (Admin)',
keywords: ['prompts', 'templates', 'ai instructions', 'system prompts', 'gpt', 'claude', 'llm'],
content: 'Manage AI prompt templates for content generation. Edit system prompts, user prompts, and prompt variables. Configure different prompts for articles, social posts, meta descriptions. Admin only.',
quickActions: [
{ label: 'Author Profiles', path: '/thinker/author-profiles' },
{ label: 'Content Settings', path: '/account/content-settings' },
]
},
{
title: 'Author Profiles',
path: '/thinker/author-profiles',
type: 'setup',
category: 'AI',
description: 'Writing style profiles (Admin)',
keywords: ['author', 'voice', 'style', 'tone', 'personality', 'writing profile', 'brand voice'],
content: 'Create author personas for different writing styles. Configure tone, vocabulary level, sentence structure preferences. Assign author profiles to content for consistent brand voice. Admin only.',
quickActions: [
{ label: 'View Prompts', path: '/thinker/prompts' },
{ label: 'Content Settings', path: '/account/content-settings' },
]
},
// Account
{ title: 'Account Settings', path: '/account/settings', type: 'account', category: 'Account' },
{ title: 'Plans & Billing', path: '/account/plans', type: 'account', category: 'Account' },
{ title: 'Usage Analytics', path: '/account/usage', type: 'account', category: 'Account' },
{ title: 'Team Management', path: '/account/settings/team', type: 'account', category: 'Account' },
{ title: 'Notifications', path: '/account/notifications', type: 'account', category: 'Account' },
{
title: 'Account Settings',
path: '/account/settings',
type: 'account',
category: 'Account',
description: 'Profile and preferences',
keywords: ['account', 'profile', 'user', 'preferences', 'settings', 'password', 'email', 'name'],
content: 'Manage your account profile and preferences. Update name, email, password. Configure notification preferences, timezone, language. View account status and subscription details.',
quickActions: [
{ label: 'Team Management', path: '/account/settings/team' },
{ label: 'Notifications', path: '/account/notifications' },
]
},
{
title: 'Plans & Billing',
path: '/account/plans',
type: 'account',
category: 'Account',
description: 'Subscription and credits',
keywords: ['billing', 'subscription', 'plan', 'credits', 'payment', 'upgrade', 'pricing', 'invoice'],
content: 'Manage subscription plan and credits. View current plan details, upgrade or downgrade. Purchase credit packs, view billing history and invoices. Configure payment methods.',
quickActions: [
{ label: 'Usage Analytics', path: '/account/usage' },
{ label: 'Purchase Credits', path: '/account/plans' },
]
},
{
title: 'Usage Analytics',
path: '/account/usage',
type: 'account',
category: 'Account',
description: 'Credit usage and insights',
keywords: ['usage', 'analytics', 'stats', 'consumption', 'credits spent', 'reports', 'metrics'],
content: 'View detailed credit usage analytics. Charts and graphs showing daily/weekly/monthly consumption. Filter by action type (content generation, images, clustering). Export usage reports.',
quickActions: [
{ label: 'View Logs', path: '/account/usage/logs' },
{ label: 'Plans & Billing', path: '/account/plans' },
]
},
{
title: 'Team Management',
path: '/account/settings/team',
type: 'account',
category: 'Account',
description: 'Invite and manage team members',
keywords: ['team', 'users', 'members', 'invite', 'permissions', 'roles', 'collaboration', 'access'],
content: 'Invite team members to your workspace. Manage user roles and permissions. View team activity, remove users, resend invitations. Configure collaboration settings and access controls.',
quickActions: [
{ label: 'Account Settings', path: '/account/settings' },
{ label: 'View Usage', path: '/account/usage' },
]
},
{
title: 'Notifications',
path: '/account/notifications',
type: 'account',
category: 'Account',
description: 'System and content notifications',
keywords: ['notifications', 'alerts', 'updates', 'email notifications', 'bell', 'messages'],
content: 'View all system notifications and content updates. Mark as read, filter by type. Configure notification preferences for email and in-app alerts. See content generation completions, publishing status, credit warnings.',
quickActions: [
{ label: 'Account Settings', path: '/account/settings' },
]
},
// Help
{ title: 'Help & Support', path: '/help', type: 'help', category: 'Help' },
{
title: 'Help & Support',
path: '/help',
type: 'help',
category: 'Help',
description: 'Documentation and support',
keywords: ['help', 'support', 'docs', 'documentation', 'guide', 'tutorial', 'faq', 'assistance'],
content: 'Access help documentation, user guides, and tutorials. Search knowledge base, view FAQs, contact support. Getting started guides, video tutorials, API documentation, and troubleshooting tips.',
quickActions: [
{ label: 'Get Started', path: '/setup/wizard' },
]
},
];
export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
@@ -86,15 +475,140 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
.filter((item): item is SearchResult => item !== undefined);
};
// Enhanced search: title, category, description, keywords, and content
const searchItems = (searchQuery: string): SearchResult[] => {
const lowerQuery = searchQuery.toLowerCase().trim();
if (!lowerQuery) return [];
return SEARCH_ITEMS.filter(item => {
const matchesFilter = activeFilter === 'all' || item.type === activeFilter;
if (!matchesFilter) return false;
// Search in title, category, description
const matchesBasic =
item.title.toLowerCase().includes(lowerQuery) ||
item.category.toLowerCase().includes(lowerQuery) ||
item.description?.toLowerCase().includes(lowerQuery);
// Search in keywords array
const matchesKeywords = item.keywords?.some(kw => kw.toLowerCase().includes(lowerQuery));
// Search in content text
const matchesContent = item.content?.toLowerCase().includes(lowerQuery);
return matchesBasic || matchesKeywords || matchesContent;
}).map(item => {
// Add context snippet around matched text
let contextSnippet = '';
// Try to find context in keywords first
const matchedKeyword = item.keywords?.find(kw => kw.toLowerCase().includes(lowerQuery));
if (matchedKeyword) {
contextSnippet = `Related: ${matchedKeyword}`;
}
// Otherwise look for context in content
else if (item.content && item.content.toLowerCase().includes(lowerQuery)) {
contextSnippet = getContextSnippet(item.content, lowerQuery);
}
return { ...item, contextSnippet };
});
};
// Get context snippet with words before and after the match
const getContextSnippet = (text: string, query: string): string => {
const lowerText = text.toLowerCase();
const index = lowerText.indexOf(query.toLowerCase());
if (index === -1) return '';
// Get ~50 chars before and after the match
const start = Math.max(0, index - 50);
const end = Math.min(text.length, index + query.length + 50);
let snippet = text.substring(start, end);
// Add ellipsis if truncated
if (start > 0) snippet = '...' + snippet;
if (end < text.length) snippet = snippet + '...';
return snippet;
};
// Normalize search query by removing common filler words
const normalizeQuery = (query: string): string[] => {
const fillerWords = ['how', 'to', 'do', 'i', 'can', 'what', 'is', 'are', 'the', 'a', 'an', 'where', 'when', 'why', 'which', 'who', 'does', 'my', 'your', 'for', 'in', 'on', 'at', 'from'];
const words = query.toLowerCase().trim().split(/\s+/);
// Filter out filler words and keep meaningful terms
const meaningfulWords = words.filter(word => !fillerWords.includes(word));
// Also handle plurals -> singular (basic stemming)
return meaningfulWords.map(word => {
if (word.endsWith('s') && word.length > 3) {
return word.slice(0, -1); // Remove 's' from end
}
return word;
});
};
// Get suggested questions based on search query
const getSuggestedQuestions = (searchQuery: string): SuggestedQuestion[] => {
if (!searchQuery || searchQuery.length < 3) return [];
const lowerQuery = searchQuery.toLowerCase().trim();
const suggestions: SuggestedQuestion[] = [];
const seenQuestions = new Set<string>(); // Prevent duplicates
// Get normalized search terms
const searchTerms = normalizeQuery(searchQuery);
// Find relevant questions from knowledge base
Object.entries(HELP_KNOWLEDGE_BASE).forEach(([keyword, questions]) => {
// Check if query matches keyword directly
const directMatch = lowerQuery.includes(keyword) || keyword.includes(lowerQuery);
// Check if any normalized search term matches
const termMatch = searchTerms.some(term =>
keyword.includes(term) || term.includes(keyword)
);
// Also check if any term appears in the question text itself
const questionTextMatch = questions.some(q =>
searchTerms.some(term => q.question.toLowerCase().includes(term))
);
if (directMatch || termMatch || questionTextMatch) {
questions.forEach(q => {
// Avoid duplicates
if (!seenQuestions.has(q.question)) {
suggestions.push(q);
seenQuestions.add(q.question);
}
});
}
});
// Limit to top 4 most relevant questions
return suggestions.slice(0, 4);
};
// Highlight matched text in string
const highlightMatch = (text: string, query: string) => {
if (!query) return text;
const parts = text.split(new RegExp(`(${query})`, 'gi'));
return parts.map((part, index) =>
part.toLowerCase() === query.toLowerCase()
? `<mark>${part}</mark>`
: part
).join('');
};
const filteredResults = query.length > 0
? SEARCH_ITEMS.filter(item => {
const matchesQuery = item.title.toLowerCase().includes(query.toLowerCase()) ||
item.category.toLowerCase().includes(query.toLowerCase());
const matchesFilter = activeFilter === 'all' || item.type === activeFilter;
return matchesQuery && matchesFilter;
})
? searchItems(query)
: (activeFilter === 'all' ? getRecentSearchResults() : SEARCH_ITEMS.filter(item => item.type === activeFilter));
const suggestedQuestions = getSuggestedQuestions(query);
useEffect(() => {
if (isOpen) {
setQuery('');
@@ -126,6 +640,23 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
onClose();
};
const handleQuickAction = (action: QuickAction, e: React.MouseEvent) => {
e.stopPropagation();
if (action.path) {
addRecentSearch(action.path);
navigate(action.path);
onClose();
} else if (action.action) {
action.action();
}
};
const handleClearSearch = () => {
setQuery('');
setSelectedIndex(0);
inputRef.current?.focus();
};
const filterOptions: { value: FilterType; label: string }[] = [
{ value: 'all', label: 'All' },
{ value: 'workflow', label: 'Workflow' },
@@ -136,31 +667,65 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
return (
<Modal isOpen={isOpen} onClose={onClose} className="sm:max-w-2xl">
<style>{searchHighlightStyles}</style>
<div className="p-0">
{/* Search Input */}
<div className="relative">
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 z-10">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</span>
{/* Using native input for ref and onKeyDown support - styled to match design system */}
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search pages..."
className="h-9 w-full rounded-lg border appearance-none px-3 py-2 text-sm shadow-theme-xs placeholder:text-gray-400 focus:outline-hidden focus:ring-3 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 bg-transparent text-gray-800 border-gray-300 focus:border-brand-300 focus:ring-brand-500/20 dark:border-gray-700 dark:focus:border-brand-800 pl-12 pr-4 py-4 text-lg border-b border-gray-200 dark:border-gray-700 rounded-none border-x-0 border-t-0"
/>
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-xs text-gray-400 hidden sm:block z-10">
ESC to close
</span>
{/* Header */}
<div className="px-6 pt-5 pb-4 border-b border-gray-200 dark:border-gray-700 bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900">
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
Quick Navigation
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
Navigate to any page in your IGNY8 workspace
</p>
</div>
<button
onClick={onClose}
className="flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-gray-200 transition-colors"
aria-label="Close search"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Search Input */}
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-brand-500 dark:text-brand-400 z-10">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</span>
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type to search pages..."
className="h-11 w-full rounded-lg border appearance-none px-3 py-2 text-sm shadow-sm placeholder:text-gray-400 focus:outline-hidden focus:ring-2 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 bg-white text-gray-900 border-gray-300 focus:border-brand-500 focus:ring-brand-500/30 dark:border-gray-700 dark:focus:border-brand-500 pl-10 pr-20"
/>
{query && (
<button
onClick={handleClearSearch}
className="absolute right-16 top-1/2 -translate-y-1/2 w-5 h-5 rounded flex items-center justify-center text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-gray-200 transition-colors z-10"
aria-label="Clear search"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-medium px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hidden sm:block z-10">
ESC
</span>
</div>
</div>
{/* Filters */}
<div className="flex gap-2 px-4 py-3 border-b border-gray-200 dark:border-gray-700 overflow-x-auto">
<div className="flex gap-2 px-4 py-3 border-b border-gray-200 dark:border-gray-700 overflow-x-auto bg-white dark:bg-gray-900">
{filterOptions.map((filter) => (
<Button
key={filter.value}
@@ -180,41 +745,185 @@ export default function SearchModal({ isOpen, onClose }: SearchModalProps) {
{/* Recent Searches Header (only when showing recent) */}
{query.length === 0 && activeFilter === 'all' && recentSearches.length > 0 && (
<div className="px-4 py-2 text-xs font-medium text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
<div className="px-4 py-2.5 text-xs font-semibold text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 flex items-center gap-2">
<svg className="w-4 h-4 text-brand-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Recent Searches
</div>
)}
{/* Results */}
<div className="max-h-80 overflow-y-auto py-2">
<div className="max-h-[500px] overflow-y-auto py-2 bg-white dark:bg-gray-900">
{filteredResults.length === 0 ? (
<div className="px-4 py-8 text-center text-gray-500">
{query.length > 0
? `No results found for "${query}"`
: 'No recent searches'}
<div className="px-4 py-16 text-center">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-gradient-to-br from-brand-100 to-brand-50 dark:from-brand-900/40 dark:to-brand-900/20 flex items-center justify-center">
<svg className="w-8 h-8 text-brand-500 dark:text-brand-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M12 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="text-sm font-medium text-gray-900 dark:text-white mb-1">
{query.length > 0
? 'No results found'
: 'No recent searches'}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{query.length > 0
? `Try searching with different keywords`
: 'Your recent page visits will appear here'}
</p>
</div>
) : (
filteredResults.map((result, index) => (
<Button
key={result.path}
variant="ghost"
tone="neutral"
onClick={() => handleSelect(result)}
className={`w-full px-4 py-3 flex items-center gap-3 text-left justify-start rounded-none ${
index === selectedIndex
? 'bg-brand-50 dark:bg-brand-900/20 text-brand-600 dark:text-brand-400'
: 'hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300'
}`}
>
<svg className="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{result.title}</div>
<div className="text-xs text-gray-500 dark:text-gray-400 truncate">{result.category}</div>
<div className="space-y-1 px-2">
{filteredResults.map((result, index) => (
<div
key={result.path}
className={`search-result group relative px-3 py-3 rounded-xl cursor-pointer transition-all ${
index === selectedIndex
? 'bg-gradient-to-r from-brand-50 to-brand-100/50 dark:from-brand-900/30 dark:to-brand-900/20 shadow-sm ring-2 ring-brand-200 dark:ring-brand-800'
: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
}`}
onClick={() => handleSelect(result)}
onMouseEnter={() => setSelectedIndex(index)}
>
<div className="flex items-start gap-3">
{/* Icon */}
<div className={`flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all ${
index === selectedIndex
? 'bg-brand-500 dark:bg-brand-600 shadow-lg shadow-brand-500/30'
: 'bg-gradient-to-br from-gray-100 to-gray-50 dark:from-gray-800 dark:to-gray-700 group-hover:from-brand-50 group-hover:to-brand-100 dark:group-hover:from-brand-900/40 dark:group-hover:to-brand-900/20'
}`}>
<svg className={`w-5 h-5 transition-colors ${
index === selectedIndex
? 'text-white'
: 'text-gray-500 dark:text-gray-400 group-hover:text-brand-600 dark:group-hover:text-brand-400'
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
{result.type === 'workflow' && (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
)}
{result.type === 'setup' && (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
)}
{result.type === 'account' && (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
)}
{result.type === 'help' && (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
)}
</svg>
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4
className={`font-semibold text-sm truncate transition-colors ${
index === selectedIndex
? 'text-brand-700 dark:text-brand-300'
: 'text-gray-900 dark:text-white'
}`}
dangerouslySetInnerHTML={{ __html: highlightMatch(result.title, query) }}
/>
<span className={`flex-shrink-0 text-xs px-2 py-0.5 rounded-full font-medium transition-colors ${
index === selectedIndex
? 'bg-brand-200 dark:bg-brand-800 text-brand-800 dark:text-brand-200'
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400'
}`}>
{result.category}
</span>
</div>
{result.description && (
<p
className="text-xs text-gray-600 dark:text-gray-400 mb-2 line-clamp-1"
dangerouslySetInnerHTML={{ __html: highlightMatch(result.description, query) }}
/>
)}
{/* Context Snippet - show matched text with surrounding context */}
{query && result.contextSnippet && (
<div className="mb-2 text-xs px-2 py-1 rounded bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 text-gray-700 dark:text-gray-300 italic">
<span dangerouslySetInnerHTML={{ __html: highlightMatch(result.contextSnippet, query) }} />
</div>
)}
{/* Quick Actions */}
{result.quickActions && result.quickActions.length > 0 && (
<div className={`flex flex-wrap gap-1.5 mt-2.5 transition-opacity ${
index === selectedIndex ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
}`}>
{result.quickActions.map((action, actionIndex) => (
<button
key={actionIndex}
onClick={(e) => handleQuickAction(action, e)}
className="text-xs px-2.5 py-1 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:bg-brand-50 hover:border-brand-300 hover:text-brand-700 dark:hover:bg-brand-900/40 dark:hover:border-brand-700 dark:hover:text-brand-300 transition-all shadow-sm hover:shadow"
>
{action.label}
</button>
))}
</div>
)}
</div>
{/* Enter hint */}
{index === selectedIndex && (
<div className="flex-shrink-0 text-xs px-2.5 py-1 rounded-lg bg-brand-200 dark:bg-brand-800 text-brand-800 dark:text-brand-200 font-semibold shadow-sm">
</div>
)}
</div>
</div>
</Button>
))
))}
</div>
)}
{/* Suggested Questions Section */}
{query.length >= 3 && suggestedQuestions.length > 0 && (
<div className="mt-2 border-t border-gray-200 dark:border-gray-700 pt-3 px-2">
<div className="flex items-center gap-2 mb-2 px-2">
<svg className="w-4 h-4 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
<h3 className="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">
Suggested Questions
</h3>
</div>
<div className="space-y-2">
{suggestedQuestions.map((item, idx) => (
<div
key={idx}
className="group px-3 py-2.5 rounded-lg bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-indigo-900/20 dark:to-purple-900/20 border border-indigo-200 dark:border-indigo-800 hover:border-indigo-300 dark:hover:border-indigo-700 cursor-pointer transition-all hover:shadow-md"
onClick={() => {
navigate(item.path);
onClose();
}}
>
<div className="flex items-start gap-2">
<svg className="w-4 h-4 text-indigo-600 dark:text-indigo-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-semibold text-indigo-700 dark:text-indigo-300 mb-1 group-hover:text-indigo-800 dark:group-hover:text-indigo-200">
{item.question}
</h4>
<p className="text-xs text-gray-600 dark:text-gray-400 leading-relaxed mb-2">
{item.answer}
</p>
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs px-2 py-0.5 rounded bg-indigo-100 dark:bg-indigo-900/40 text-indigo-700 dark:text-indigo-300 font-medium">
📖 {item.helpSection}
</span>
<span className="text-xs text-indigo-600 dark:text-indigo-400 group-hover:text-indigo-700 dark:group-hover:text-indigo-300 transition-colors font-medium">
Read detailed guide
</span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>

View File

@@ -1,10 +1,11 @@
import React, { useState, ReactNode } from 'react';
import React, { useState, useEffect, ReactNode } from 'react';
import { ChevronDownIcon } from '../../../icons';
interface AccordionItemProps {
title: string;
children: ReactNode;
defaultOpen?: boolean;
forceOpen?: boolean; // External control to force open (for deep linking)
className?: string;
}
@@ -12,10 +13,18 @@ export const AccordionItem: React.FC<AccordionItemProps> = ({
title,
children,
defaultOpen = false,
forceOpen = false,
className = '',
}) => {
const [isOpen, setIsOpen] = useState(defaultOpen);
// Force open when forceOpen prop changes
useEffect(() => {
if (forceOpen) {
setIsOpen(true);
}
}, [forceOpen]);
return (
<div className={`border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden ${className}`}>
<button

View File

@@ -1,4 +1,4 @@
import { useState, useRef } from "react";
import { useState, useRef, useEffect } from "react";
import PageMeta from "../../components/common/PageMeta";
import PageHeader from "../../components/common/PageHeader";
import { Accordion, AccordionItem } from "../../components/ui/accordion";
@@ -13,6 +13,7 @@ import {
GroupIcon,
HelpCircleIcon
} from "../../icons";
import { useLocation } from "react-router-dom";
interface TableOfContentsItem {
id: string;
@@ -143,7 +144,40 @@ function ModuleCard({ title, icon, color, children }: { title: string; icon: Rea
export default function Help() {
const [activeSection, setActiveSection] = useState<string | null>(null);
const [openAccordions, setOpenAccordions] = useState<Set<string>>(new Set());
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({});
const location = useLocation();
// Handle URL hash navigation and auto-expand accordions
useEffect(() => {
const hash = location.hash.replace('#', '');
if (hash) {
// Small delay to ensure DOM is ready
setTimeout(() => {
scrollToSection(hash, true);
}, 100);
}
}, [location.hash]);
const scrollToSection = (id: string, fromHash = false) => {
const element = sectionRefs.current[id];
if (element) {
// Open the accordion if the section is inside one
if (fromHash) {
setOpenAccordions(prev => new Set([...prev, id]));
}
const offset = 100;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({
top: offsetPosition,
behavior: "smooth"
});
setActiveSection(id);
}
};
const tableOfContents: TableOfContentsItem[] = [
{ id: "getting-started", title: "Getting Started", level: 1 },
@@ -177,21 +211,6 @@ export default function Help() {
{ id: "faq", title: "Frequently Asked Questions", level: 1 },
];
const scrollToSection = (id: string) => {
const element = sectionRefs.current[id];
if (element) {
const offset = 100;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({
top: offsetPosition,
behavior: "smooth"
});
setActiveSection(id);
}
};
const faqItems = [
{
question: "How do I add keywords to my workflow?",
@@ -551,8 +570,8 @@ export default function Help() {
</div>
</AccordionItem>
<AccordionItem title="Add Keywords">
<div className="space-y-4">
<AccordionItem title="Add Keywords" forceOpen={openAccordions.has('importing-keywords')}>
<div id="importing-keywords" ref={(el) => (sectionRefs.current["importing-keywords"] = el)} className="space-y-4 scroll-mt-24">
<p className="text-gray-700 dark:text-gray-300">
Browse and add keywords from our curated database organized by 100+ industry sectors.
</p>
@@ -578,8 +597,8 @@ export default function Help() {
</div>
</AccordionItem>
<AccordionItem title="Content Settings">
<div className="space-y-4">
<AccordionItem title="Content Settings" forceOpen={openAccordions.has('content-settings')}>
<div id="content-settings" ref={(el) => (sectionRefs.current["content-settings"] = el)} className="space-y-4 scroll-mt-24">
<p className="text-gray-700 dark:text-gray-300">
Configure how AI generates and publishes your content.
</p>
@@ -669,8 +688,8 @@ export default function Help() {
</h2>
<Accordion>
<AccordionItem title="Keywords Management" defaultOpen>
<div className="space-y-4">
<AccordionItem title="Keywords Management" defaultOpen forceOpen={openAccordions.has('managing-keywords')}>
<div id="managing-keywords" ref={(el) => (sectionRefs.current["managing-keywords"] = el)} className="space-y-4 scroll-mt-24">
<p className="text-gray-700 dark:text-gray-300">
Keywords are the foundation of your content strategy. Manage, filter, and organize your keywords here.
</p>
@@ -709,8 +728,8 @@ export default function Help() {
</div>
</AccordionItem>
<AccordionItem title="Keyword Clusters">
<div className="space-y-4">
<AccordionItem title="Keyword Clusters" forceOpen={openAccordions.has('keyword-clustering')}>
<div id="keyword-clustering" ref={(el) => (sectionRefs.current["keyword-clustering"] = el)} className="space-y-4 scroll-mt-24">
<p className="text-gray-700 dark:text-gray-300">
Clusters group related keywords for comprehensive content planning and topical authority building.
</p>
@@ -779,8 +798,8 @@ export default function Help() {
</h2>
<Accordion>
<AccordionItem title="Tasks Management" defaultOpen>
<div className="space-y-4">
<AccordionItem title="Tasks Management" defaultOpen forceOpen={openAccordions.has('editing-content')}>
<div id="editing-content" ref={(el) => (sectionRefs.current["editing-content"] = el)} className="space-y-4 scroll-mt-24">
<p className="text-gray-700 dark:text-gray-300">
Tasks are content ideas converted into actionable writing assignments with status tracking.
</p>
@@ -808,8 +827,8 @@ export default function Help() {
</div>
</AccordionItem>
<AccordionItem title="Content Generation">
<div className="space-y-4">
<AccordionItem title="Content Generation" forceOpen={openAccordions.has('content-generation')}>
<div id="content-generation" ref={(el) => (sectionRefs.current["content-generation"] = el)} className="space-y-4 scroll-mt-24">
<p className="text-gray-700 dark:text-gray-300">
Generate, edit, and manage your AI-created content.
</p>
@@ -858,8 +877,10 @@ export default function Help() {
</div>
</AccordionItem>
<AccordionItem title="Image Generation">
<div className="space-y-4">
<AccordionItem title="Image Generation" forceOpen={openAccordions.has('image-generation') || openAccordions.has('image-settings') || openAccordions.has('managing-images')}>
<div id="image-generation" ref={(el) => (sectionRefs.current["image-generation"] = el)} className="space-y-4 scroll-mt-24">
<div id="image-settings" ref={(el) => (sectionRefs.current["image-settings"] = el)}></div>
<div id="managing-images" ref={(el) => (sectionRefs.current["managing-images"] = el)}></div>
<p className="text-gray-700 dark:text-gray-300">
Generate AI images for your content using DALL-E 3 (premium) or Runware (basic).
</p>
@@ -892,8 +913,8 @@ export default function Help() {
</div>
</AccordionItem>
<AccordionItem title="Review & Publish">
<div className="space-y-4">
<AccordionItem title="Review & Publish" forceOpen={openAccordions.has('content-workflow')}>
<div id="content-workflow" ref={(el) => (sectionRefs.current["content-workflow"] = el)} className="space-y-4 scroll-mt-24">
<p className="text-gray-700 dark:text-gray-300">
Final review stage before publishing to WordPress.
</p>
@@ -932,6 +953,8 @@ export default function Help() {
{/* Automation Section */}
<div ref={(el) => (sectionRefs.current["automation"] = el)} className="mb-12 scroll-mt-24">
<div id="automation-setup" ref={(el) => (sectionRefs.current["automation-setup"] = el)}></div>
<div id="auto-publishing" ref={(el) => (sectionRefs.current["auto-publishing"] = el)}></div>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-3">
<BoltIcon className="size-8 text-warning-600 dark:text-warning-400" />
Automation Pipeline
@@ -1022,8 +1045,9 @@ export default function Help() {
</h2>
<Accordion>
<AccordionItem title="WordPress Integration" defaultOpen>
<div className="space-y-4">
<AccordionItem title="WordPress Integration" defaultOpen forceOpen={openAccordions.has('publishing-wordpress') || openAccordions.has('wordpress-integration')}>
<div id="publishing-wordpress" ref={(el) => (sectionRefs.current["publishing-wordpress"] = el)} className="space-y-4 scroll-mt-24">
<div id="wordpress-integration" ref={(el) => (sectionRefs.current["wordpress-integration"] = el)}></div>
<p className="text-gray-700 dark:text-gray-300">
Connect your WordPress site for seamless content publishing.
</p>
@@ -1063,8 +1087,9 @@ export default function Help() {
</div>
</AccordionItem>
<AccordionItem title="AI Providers">
<div className="space-y-4">
<AccordionItem title="AI Providers" forceOpen={openAccordions.has('prompt-management') || openAccordions.has('author-profiles')}>
<div id="prompt-management" ref={(el) => (sectionRefs.current["prompt-management"] = el)} className="space-y-4 scroll-mt-24">
<div id="author-profiles" ref={(el) => (sectionRefs.current["author-profiles"] = el)}></div>
<p className="text-gray-700 dark:text-gray-300">
IGNY8 integrates with multiple AI providers for content and image generation.
</p>
@@ -1100,8 +1125,10 @@ export default function Help() {
</h2>
<Accordion>
<AccordionItem title="Credits System" defaultOpen>
<div className="space-y-4">
<AccordionItem title="Credits System" defaultOpen forceOpen={openAccordions.has('credit-system') || openAccordions.has('purchasing-credits') || openAccordions.has('usage-tracking')}>
<div id="credit-system" ref={(el) => (sectionRefs.current["credit-system"] = el)} className="space-y-4 scroll-mt-24">
<div id="purchasing-credits" ref={(el) => (sectionRefs.current["purchasing-credits"] = el)}></div>
<div id="usage-tracking" ref={(el) => (sectionRefs.current["usage-tracking"] = el)}></div>
<p className="text-gray-700 dark:text-gray-300">
Credits are your currency for AI operations. Understand how credits work:
</p>
@@ -1212,8 +1239,9 @@ export default function Help() {
</div>
</AccordionItem>
<AccordionItem title="Team Management">
<div className="space-y-4">
<AccordionItem title="Team Management" forceOpen={openAccordions.has('team-collaboration') || openAccordions.has('user-roles')}>
<div id="team-collaboration" ref={(el) => (sectionRefs.current["team-collaboration"] = el)} className="space-y-4 scroll-mt-24">
<div id="user-roles" ref={(el) => (sectionRefs.current["user-roles"] = el)}></div>
<p className="text-gray-700 dark:text-gray-300">
Invite team members and manage roles in Account Settings Team.
</p>