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:
662
docs/plans/PHASE-6-BACKUP-CLEANUP-GUIDE.md
Normal file
662
docs/plans/PHASE-6-BACKUP-CLEANUP-GUIDE.md
Normal 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)*
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user