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:
@@ -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