Vyuh CDX

Help System

Context-sensitive help system for effective user assistance

Help System Integration

The Vyuh Entity System includes a comprehensive help system that provides context-sensitive assistance throughout your application. This guide covers help content management, UI integration, and best practices for creating effective help documentation.

Help System Overview

The help system provides:

  • Context-sensitive help for entities and features
  • Interactive tutorials and walkthroughs
  • Searchable documentation
  • Video guides and tooltips
  • User onboarding flows

Core Components

Help Models

class HelpContent extends EntityBase {
  final String key;
  final String title;
  final String content;
  final HelpContentType type;
  final List<String> tags;
  final Map<String, dynamic>? metadata;
  final List<HelpSection>? sections;
  final List<HelpLink>? relatedLinks;
  final String? videoUrl;
  final int? displayOrder;
  
  HelpContent({
    required super.id,
    required super.schemaType,
    required this.key,
    required this.title,
    required this.content,
    required this.type,
    this.tags = const [],
    this.metadata,
    this.sections,
    this.relatedLinks,
    this.videoUrl,
    this.displayOrder,
    super.createdAt,
    super.layout,
    super.modifiers,
  });
  
  factory HelpContent.fromJson(Map<String, dynamic> json) => 
    _$HelpContentFromJson(json);
    
  @override
  Map<String, dynamic> toJson() => _$HelpContentToJson(this);
}

enum HelpContentType {
  overview,
  tutorial,
  reference,
  faq,
  video,
  tip,
}

class HelpSection {
  final String title;
  final String content;
  final List<HelpSection>? subsections;
  final String? anchor;
  
  const HelpSection({
    required this.title,
    required this.content,
    this.subsections,
    this.anchor,
  });
}

class HelpLink {
  final String title;
  final String url;
  final HelpLinkType type;
  final String? description;
  
  const HelpLink({
    required this.title,
    required this.url,
    required this.type,
    this.description,
  });
}

enum HelpLinkType {
  internal,
  external,
  video,
  documentation,
}

HelpService

class HelpService {
  final HelpRepository repository;
  final Map<String, HelpContent> _cache = {};
  
  HelpService({required this.repository});
  
  Future<HelpContent?> getHelp(String key) async {
    // Check cache
    if (_cache.containsKey(key)) {
      return _cache[key];
    }
    
    // Load from repository
    final help = await repository.getByKey(key);
    if (help != null) {
      _cache[key] = help;
    }
    
    return help;
  }
  
  Future<List<HelpContent>> search(String query) async {
    return repository.search(query);
  }
  
  Future<List<HelpContent>> getByTags(List<String> tags) async {
    return repository.getByTags(tags);
  }
  
  Future<List<HelpContent>> getForEntity(String entityType) async {
    return repository.getByTags([entityType, 'entity']);
  }
  
  Future<Map<String, List<HelpContent>>> getGroupedHelp() async {
    final allHelp = await repository.getAll();
    
    return groupBy(allHelp, (help) {
      if (help.type == HelpContentType.faq) return 'FAQ';
      if (help.type == HelpContentType.tutorial) return 'Tutorials';
      if (help.type == HelpContentType.video) return 'Videos';
      return 'Documentation';
    });
  }
  
  void clearCache() {
    _cache.clear();
  }
}

abstract class HelpRepository {
  Future<HelpContent?> getByKey(String key);
  Future<List<HelpContent>> search(String query);
  Future<List<HelpContent>> getByTags(List<String> tags);
  Future<List<HelpContent>> getAll();
  Future<void> save(HelpContent content);
  Future<void> delete(String id);
}

Help UI Components

HelpButton

Context-aware help button:

class HelpButton extends StatelessWidget {
  final String helpKey;
  final String? fallbackUrl;
  final HelpDisplayMode displayMode;
  
  const HelpButton({
    required this.helpKey,
    this.fallbackUrl,
    this.displayMode = HelpDisplayMode.dialog,
  });
  
  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: const Icon(Icons.help_outline),
      tooltip: 'Help',
      onPressed: () => _showHelp(context),
    );
  }
  
  Future<void> _showHelp(BuildContext context) async {
    final helpService = context.read<HelpService>();
    final help = await helpService.getHelp(helpKey);
    
    if (help == null && fallbackUrl != null) {
      // Open fallback URL
      await launchUrl(Uri.parse(fallbackUrl!));
      return;
    }
    
    if (help == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Help content not found')),
      );
      return;
    }
    
    switch (displayMode) {
      case HelpDisplayMode.dialog:
        showDialog(
          context: context,
          builder: (_) => HelpDialog(content: help),
        );
        break;
        
      case HelpDisplayMode.bottomSheet:
        showModalBottomSheet(
          context: context,
          isScrollControlled: true,
          builder: (_) => HelpBottomSheet(content: help),
        );
        break;
        
      case HelpDisplayMode.page:
        context.go('/help/${help.id}');
        break;
        
      case HelpDisplayMode.tooltip:
        // Show as overlay tooltip
        _showTooltip(context, help);
        break;
    }
  }
  
  void _showTooltip(BuildContext context, HelpContent help) {
    final overlay = Overlay.of(context);
    final renderBox = context.findRenderObject() as RenderBox;
    final position = renderBox.localToGlobal(Offset.zero);
    
    final entry = OverlayEntry(
      builder: (context) => HelpTooltip(
        content: help,
        targetPosition: position,
        targetSize: renderBox.size,
      ),
    );
    
    overlay.insert(entry);
    
    // Remove after delay or on tap
    Future.delayed(const Duration(seconds: 5), () {
      entry.remove();
    });
  }
}

enum HelpDisplayMode {
  dialog,
  bottomSheet,
  page,
  tooltip,
}

HelpDialog

Full-featured help dialog:

class HelpDialog extends StatefulWidget {
  final HelpContent content;
  
  const HelpDialog({required this.content});
  
  @override
  State<HelpDialog> createState() => _HelpDialogState();
}

class _HelpDialogState extends State<HelpDialog> {
  String? _selectedSection;
  
  @override
  Widget build(BuildContext context) {
    return Dialog(
      child: Container(
        width: 800,
        height: 600,
        child: Column(
          children: [
            // Header
            Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Theme.of(context).primaryColor,
                borderRadius: const BorderRadius.vertical(
                  top: Radius.circular(12),
                ),
              ),
              child: Row(
                children: [
                  Icon(
                    _getIconForType(widget.content.type),
                    color: Colors.white,
                  ),
                  const SizedBox(width: 8),
                  Expanded(
                    child: Text(
                      widget.content.title,
                      style: Theme.of(context).textTheme.titleLarge?.copyWith(
                        color: Colors.white,
                      ),
                    ),
                  ),
                  IconButton(
                    icon: const Icon(Icons.close, color: Colors.white),
                    onPressed: () => Navigator.of(context).pop(),
                  ),
                ],
              ),
            ),
            
            // Content
            Expanded(
              child: Row(
                children: [
                  // Table of contents
                  if (widget.content.sections != null &&
                      widget.content.sections!.isNotEmpty)
                    Container(
                      width: 250,
                      decoration: BoxDecoration(
                        border: Border(
                          right: BorderSide(
                            color: Theme.of(context).dividerColor,
                          ),
                        ),
                      ),
                      child: _buildTableOfContents(),
                    ),
                  
                  // Main content
                  Expanded(
                    child: SingleChildScrollView(
                      padding: const EdgeInsets.all(24),
                      child: _buildContent(),
                    ),
                  ),
                ],
              ),
            ),
            
            // Footer
            if (widget.content.relatedLinks != null &&
                widget.content.relatedLinks!.isNotEmpty)
              Container(
                padding: const EdgeInsets.all(16),
                decoration: BoxDecoration(
                  border: Border(
                    top: BorderSide(
                      color: Theme.of(context).dividerColor,
                    ),
                  ),
                ),
                child: _buildRelatedLinks(),
              ),
          ],
        ),
      ),
    );
  }
  
  Widget _buildTableOfContents() {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        Text(
          'Contents',
          style: Theme.of(context).textTheme.titleMedium,
        ),
        const SizedBox(height: 16),
        ...widget.content.sections!.map((section) => ListTile(
          title: Text(section.title),
          selected: _selectedSection == section.anchor,
          onTap: () {
            setState(() {
              _selectedSection = section.anchor;
            });
            // Scroll to section
            _scrollToSection(section.anchor);
          },
        )),
      ],
    );
  }
  
  Widget _buildContent() {
    if (_selectedSection != null) {
      final section = widget.content.sections?.firstWhere(
        (s) => s.anchor == _selectedSection,
      );
      if (section != null) {
        return _buildSection(section);
      }
    }
    
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // Main content
        MarkdownBody(
          data: widget.content.content,
          styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)),
          onTapLink: (text, href, title) {
            if (href != null) {
              launchUrl(Uri.parse(href));
            }
          },
        ),
        
        // Sections
        if (widget.content.sections != null)
          ...widget.content.sections!.map((section) => Padding(
            padding: const EdgeInsets.only(top: 32),
            child: _buildSection(section),
          )),
        
        // Video
        if (widget.content.videoUrl != null) ...[
          const SizedBox(height: 32),
          _buildVideoSection(),
        ],
      ],
    );
  }
  
  Widget _buildSection(HelpSection section) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          section.title,
          style: Theme.of(context).textTheme.titleLarge,
        ),
        const SizedBox(height: 16),
        MarkdownBody(
          data: section.content,
          styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)),
        ),
        
        // Subsections
        if (section.subsections != null)
          ...section.subsections!.map((subsection) => Padding(
            padding: const EdgeInsets.only(top: 16, left: 16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  subsection.title,
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                const SizedBox(height: 8),
                MarkdownBody(
                  data: subsection.content,
                  styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)),
                ),
              ],
            ),
          )),
      ],
    );
  }
  
  Widget _buildVideoSection() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                const Icon(Icons.play_circle_outline),
                const SizedBox(width: 8),
                Text(
                  'Video Tutorial',
                  style: Theme.of(context).textTheme.titleMedium,
                ),
              ],
            ),
            const SizedBox(height: 16),
            ElevatedButton.icon(
              onPressed: () {
                launchUrl(Uri.parse(widget.content.videoUrl!));
              },
              icon: const Icon(Icons.play_arrow),
              label: const Text('Watch Video'),
            ),
          ],
        ),
      ),
    );
  }
  
  Widget _buildRelatedLinks() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'Related Topics',
          style: Theme.of(context).textTheme.titleSmall,
        ),
        const SizedBox(height: 8),
        Wrap(
          spacing: 16,
          children: widget.content.relatedLinks!.map((link) => TextButton.icon(
            onPressed: () => _openLink(link),
            icon: Icon(_getIconForLinkType(link.type)),
            label: Text(link.title),
          )).toList(),
        ),
      ],
    );
  }
  
  void _openLink(HelpLink link) {
    switch (link.type) {
      case HelpLinkType.internal:
        Navigator.of(context).pop();
        context.go(link.url);
        break;
        
      case HelpLinkType.external:
      case HelpLinkType.documentation:
      case HelpLinkType.video:
        launchUrl(Uri.parse(link.url));
        break;
    }
  }
  
  IconData _getIconForType(HelpContentType type) {
    switch (type) {
      case HelpContentType.overview:
        return Icons.info_outline;
      case HelpContentType.tutorial:
        return Icons.school;
      case HelpContentType.reference:
        return Icons.book;
      case HelpContentType.faq:
        return Icons.question_answer;
      case HelpContentType.video:
        return Icons.play_circle_outline;
      case HelpContentType.tip:
        return Icons.lightbulb_outline;
    }
  }
  
  IconData _getIconForLinkType(HelpLinkType type) {
    switch (type) {
      case HelpLinkType.internal:
        return Icons.arrow_forward;
      case HelpLinkType.external:
        return Icons.open_in_new;
      case HelpLinkType.video:
        return Icons.play_circle_outline;
      case HelpLinkType.documentation:
        return Icons.description;
    }
  }
  
  void _scrollToSection(String? anchor) {
    // Implementation depends on your scrolling setup
  }
}

Interactive Tutorials

class InteractiveTutorial {
  final String id;
  final String title;
  final List<TutorialStep> steps;
  final Map<String, dynamic>? metadata;
  
  const InteractiveTutorial({
    required this.id,
    required this.title,
    required this.steps,
    this.metadata,
  });
}

class TutorialStep {
  final String title;
  final String description;
  final String? targetKey;
  final TutorialAction? action;
  final TutorialStepPosition position;
  final bool skippable;
  
  const TutorialStep({
    required this.title,
    required this.description,
    this.targetKey,
    this.action,
    this.position = TutorialStepPosition.center,
    this.skippable = true,
  });
}

enum TutorialStepPosition {
  center,
  top,
  bottom,
  left,
  right,
  target,
}

class TutorialAction {
  final String label;
  final VoidCallback onTap;
  
  const TutorialAction({
    required this.label,
    required this.onTap,
  });
}

class TutorialOverlay extends StatefulWidget {
  final InteractiveTutorial tutorial;
  final VoidCallback onComplete;
  final VoidCallback onSkip;
  
  const TutorialOverlay({
    required this.tutorial,
    required this.onComplete,
    required this.onSkip,
  });
  
  @override
  State<TutorialOverlay> createState() => _TutorialOverlayState();
}

class _TutorialOverlayState extends State<TutorialOverlay> {
  int _currentStep = 0;
  
  @override
  Widget build(BuildContext context) {
    final step = widget.tutorial.steps[_currentStep];
    
    return Stack(
      children: [
        // Dark overlay
        Container(
          color: Colors.black54,
        ),
        
        // Spotlight on target
        if (step.targetKey != null)
          _buildSpotlight(step.targetKey!),
        
        // Tutorial content
        Positioned(
          top: 0,
          left: 0,
          right: 0,
          bottom: 0,
          child: _buildStepContent(step),
        ),
      ],
    );
  }
  
  Widget _buildSpotlight(String targetKey) {
    // Find target widget and cut hole in overlay
    return CustomPaint(
      painter: SpotlightPainter(
        targetKey: targetKey,
      ),
    );
  }
  
  Widget _buildStepContent(TutorialStep step) {
    return Center(
      child: Card(
        margin: const EdgeInsets.all(32),
        child: Container(
          constraints: const BoxConstraints(maxWidth: 400),
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                step.title,
                style: Theme.of(context).textTheme.headlineSmall,
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 16),
              Text(
                step.description,
                style: Theme.of(context).textTheme.bodyLarge,
                textAlign: TextAlign.center,
              ),
              if (step.action != null) ...[
                const SizedBox(height: 24),
                ElevatedButton(
                  onPressed: step.action!.onTap,
                  child: Text(step.action!.label),
                ),
              ],
              const SizedBox(height: 32),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  if (step.skippable)
                    TextButton(
                      onPressed: widget.onSkip,
                      child: const Text('Skip Tutorial'),
                    )
                  else
                    const SizedBox(),
                  Row(
                    children: [
                      if (_currentStep > 0)
                        TextButton(
                          onPressed: _previousStep,
                          child: const Text('Previous'),
                        ),
                      const SizedBox(width: 8),
                      ElevatedButton(
                        onPressed: _nextStep,
                        child: Text(
                          _currentStep == widget.tutorial.steps.length - 1
                            ? 'Complete'
                            : 'Next',
                        ),
                      ),
                    ],
                  ),
                ],
              ),
              const SizedBox(height: 16),
              // Progress indicator
              LinearProgressIndicator(
                value: (_currentStep + 1) / widget.tutorial.steps.length,
              ),
            ],
          ),
        ),
      ),
    );
  }
  
  void _previousStep() {
    setState(() {
      _currentStep = (_currentStep - 1).clamp(0, widget.tutorial.steps.length - 1);
    });
  }
  
  void _nextStep() {
    if (_currentStep == widget.tutorial.steps.length - 1) {
      widget.onComplete();
    } else {
      setState(() {
        _currentStep++;
      });
    }
  }
}

Entity Help Integration

Automatic Help Registration

class EntityHelpIntegration {
  static void registerEntityHelp<T extends EntityBase>(
    EntityConfiguration<T> config,
    HelpService helpService,
  ) {
    final metadata = config.metadata;
    
    // Use getHelp function if provided
    if (metadata.getHelp != null) {
      // Dynamic help content
      return;
    }
    
    // Generate default help content
    final helpContent = HelpContent(
      id: 'entity_${metadata.identifier}',
      schemaType: 'help',
      key: 'entity.${metadata.identifier}',
      title: '${metadata.pluralName} Overview',
      content: _generateDefaultContent(metadata),
      type: HelpContentType.overview,
      tags: [
        metadata.identifier,
        'entity',
        metadata.category ?? 'general',
      ],
      sections: [
        HelpSection(
          title: 'What are ${metadata.pluralName}?',
          content: metadata.description ?? 
            'Manage ${metadata.pluralName.toLowerCase()} in your system.',
          anchor: 'overview',
        ),
        HelpSection(
          title: 'Common Actions',
          content: _generateActionsContent(config),
          anchor: 'actions',
        ),
        HelpSection(
          title: 'Permissions',
          content: _generatePermissionsContent(metadata),
          anchor: 'permissions',
        ),
      ],
      relatedLinks: [
        HelpLink(
          title: 'View All ${metadata.pluralName}',
          url: metadata.route.list(null),
          type: HelpLinkType.internal,
        ),
        HelpLink(
          title: 'Create ${metadata.name}',
          url: metadata.route.create(null),
          type: HelpLinkType.internal,
        ),
      ],
      createdAt: DateTime.now(),
    );
    
    // Save to help service
    helpService.repository.save(helpContent);
  }
  
  static String _generateDefaultContent(EntityMetadata metadata) {
    return '''
# ${metadata.pluralName}

${metadata.description ?? 'Manage ${metadata.pluralName.toLowerCase()} in your system.'}

## Getting Started

To work with ${metadata.pluralName.toLowerCase()}, you can:

1. **View all ${metadata.pluralName.toLowerCase()}** - Browse and search existing records
2. **Create new ${metadata.name.toLowerCase()}** - Add new records to the system
3. **Edit existing records** - Update information as needed
4. **Delete records** - Remove records that are no longer needed

## Navigation

You can access ${metadata.pluralName} from:
- The main menu under "${metadata.category ?? 'Entities'}"
- The command palette (Cmd/Ctrl+K) by searching for "${metadata.name}"
- Direct URL: `/${metadata.identifier}`
''';
  }
  
  static String _generateActionsContent<T>(EntityConfiguration<T> config) {
    final actions = <String>[];
    
    actions.add('### Standard Actions\n');
    actions.add('- **Create**: Add new ${config.metadata.name.toLowerCase()}');
    actions.add('- **Edit**: Modify existing records');
    actions.add('- **Delete**: Remove records');
    actions.add('- **Export**: Download data in various formats');
    
    if (config.actions?.list != null) {
      actions.add('\n### Custom Actions\n');
      for (final action in config.actions!.list!) {
        actions.add('- **${action.label}**: Custom action for this entity type');
      }
    }
    
    return actions.join('\n');
  }
  
  static String _generatePermissionsContent(EntityMetadata metadata) {
    return '''
The following permissions control access to ${metadata.pluralName}:

- `${metadata.identifier}.create` - Create new records
- `${metadata.identifier}.read` - View records
- `${metadata.identifier}.update` - Edit existing records
- `${metadata.identifier}.delete` - Delete records
- `${metadata.identifier}.export` - Export data
- `${metadata.identifier}.import` - Import data

Contact your administrator if you need additional permissions.
''';
  }
}

Context-Sensitive Help

class ContextualHelpProvider extends InheritedWidget {
  final Map<String, String> helpKeys;
  
  const ContextualHelpProvider({
    required this.helpKeys,
    required super.child,
  });
  
  static String? getHelpKey(BuildContext context, String widgetKey) {
    final provider = context.dependOnInheritedWidgetOfExactType<ContextualHelpProvider>();
    return provider?.helpKeys[widgetKey];
  }
  
  @override
  bool updateShouldNotify(ContextualHelpProvider oldWidget) {
    return helpKeys != oldWidget.helpKeys;
  }
}

// Usage in entity views
class EntityDetailView<T extends EntityBase> extends StatelessWidget {
  final EntityConfiguration<T> configuration;
  final String entityId;
  
  @override
  Widget build(BuildContext context) {
    return ContextualHelpProvider(
      helpKeys: {
        'detail_view': 'entity.${configuration.metadata.identifier}.detail',
        'edit_button': 'entity.${configuration.metadata.identifier}.edit',
        'delete_button': 'entity.${configuration.metadata.identifier}.delete',
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text(configuration.metadata.name),
          actions: [
            HelpButton(
              helpKey: ContextualHelpProvider.getHelpKey(
                context,
                'detail_view',
              ) ?? 'entity.detail',
            ),
          ],
        ),
        body: // ... entity detail content
      ),
    );
  }
}

Searchable Help Center

class HelpCenterPage extends StatefulWidget {
  @override
  State<HelpCenterPage> createState() => _HelpCenterPageState();
}

class _HelpCenterPageState extends State<HelpCenterPage> {
  final _searchController = TextEditingController();
  List<HelpContent> _searchResults = [];
  Map<String, List<HelpContent>>? _groupedHelp;
  
  @override
  void initState() {
    super.initState();
    _loadHelp();
  }
  
  Future<void> _loadHelp() async {
    final helpService = context.read<HelpService>();
    final grouped = await helpService.getGroupedHelp();
    
    setState(() {
      _groupedHelp = grouped;
    });
  }
  
  Future<void> _search(String query) async {
    if (query.isEmpty) {
      setState(() {
        _searchResults = [];
      });
      return;
    }
    
    final helpService = context.read<HelpService>();
    final results = await helpService.search(query);
    
    setState(() {
      _searchResults = results;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Help Center'),
      ),
      body: Column(
        children: [
          // Search bar
          Container(
            padding: const EdgeInsets.all(16),
            child: TextField(
              controller: _searchController,
              decoration: InputDecoration(
                hintText: 'Search help topics...',
                prefixIcon: const Icon(Icons.search),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(8),
                ),
                filled: true,
              ),
              onChanged: _search,
            ),
          ),
          
          // Content
          Expanded(
            child: _searchController.text.isNotEmpty
              ? _buildSearchResults()
              : _buildGroupedHelp(),
          ),
        ],
      ),
    );
  }
  
  Widget _buildSearchResults() {
    if (_searchResults.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.search_off,
              size: 64,
              color: Theme.of(context).disabledColor,
            ),
            const SizedBox(height: 16),
            Text(
              'No results found',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),
            Text(
              'Try different keywords',
              style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                color: Theme.of(context).textTheme.bodySmall?.color,
              ),
            ),
          ],
        ),
      );
    }
    
    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: _searchResults.length,
      itemBuilder: (context, index) {
        final help = _searchResults[index];
        return _HelpCard(
          content: help,
          onTap: () => _openHelp(help),
        );
      },
    );
  }
  
  Widget _buildGroupedHelp() {
    if (_groupedHelp == null) {
      return const Center(child: CircularProgressIndicator());
    }
    
    return ListView(
      padding: const EdgeInsets.all(16),
      children: _groupedHelp!.entries.map((entry) {
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 8),
              child: Text(
                entry.key,
                style: Theme.of(context).textTheme.titleLarge,
              ),
            ),
            ...entry.value.map((help) => _HelpCard(
              content: help,
              onTap: () => _openHelp(help),
            )),
            const SizedBox(height: 24),
          ],
        );
      }).toList(),
    );
  }
  
  void _openHelp(HelpContent help) {
    showDialog(
      context: context,
      builder: (_) => HelpDialog(content: help),
    );
  }
}

class _HelpCard extends StatelessWidget {
  final HelpContent content;
  final VoidCallback onTap;
  
  const _HelpCard({
    required this.content,
    required this.onTap,
  });
  
  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(bottom: 8),
      child: ListTile(
        leading: Icon(_getIconForType(content.type)),
        title: Text(content.title),
        subtitle: Text(
          content.content.split('\n').first,
          maxLines: 2,
          overflow: TextOverflow.ellipsis,
        ),
        trailing: const Icon(Icons.arrow_forward),
        onTap: onTap,
      ),
    );
  }
  
  IconData _getIconForType(HelpContentType type) {
    // Same as in HelpDialog
  }
}

Onboarding System

User Onboarding Flow

class OnboardingService {
  final SharedPreferences prefs;
  final TutorialService tutorialService;
  
  OnboardingService({
    required this.prefs,
    required this.tutorialService,
  });
  
  Future<bool> shouldShowOnboarding() async {
    return !prefs.getBool('onboarding_completed') ?? true;
  }
  
  Future<void> startOnboarding(BuildContext context) async {
    final tutorial = InteractiveTutorial(
      id: 'onboarding',
      title: 'Welcome to ${AppConfig.appName}',
      steps: [
        TutorialStep(
          title: 'Welcome!',
          description: 'Let\'s take a quick tour to get you started.',
          position: TutorialStepPosition.center,
        ),
        TutorialStep(
          title: 'Navigation',
          description: 'Use the sidebar to navigate between different sections.',
          targetKey: 'navigation_drawer',
          position: TutorialStepPosition.target,
        ),
        TutorialStep(
          title: 'Quick Search',
          description: 'Press Cmd/Ctrl+K to quickly search and navigate.',
          action: TutorialAction(
            label: 'Try It',
            onTap: () => CommandPaletteService.show(context),
          ),
        ),
        TutorialStep(
          title: 'Create Your First Entity',
          description: 'Click the + button to create your first record.',
          targetKey: 'create_button',
          position: TutorialStepPosition.target,
        ),
        TutorialStep(
          title: 'Get Help',
          description: 'Click the help icon anytime for context-sensitive help.',
          targetKey: 'help_button',
          position: TutorialStepPosition.target,
        ),
        TutorialStep(
          title: 'You\'re Ready!',
          description: 'That\'s all you need to get started. Enjoy!',
          position: TutorialStepPosition.center,
          skippable: false,
        ),
      ],
    );
    
    tutorialService.start(
      tutorial,
      onComplete: () async {
        await prefs.setBool('onboarding_completed', true);
        if (context.mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('Welcome aboard! 🎉'),
            ),
          );
        }
      },
    );
  }
  
  Future<void> resetOnboarding() async {
    await prefs.remove('onboarding_completed');
  }
}

Best Practices

  1. Keep Content Updated - Regularly review and update help content
  2. Use Clear Language - Write for your target audience
  3. Include Examples - Show, don't just tell
  4. Provide Context - Help should be relevant to current task
  5. Make It Searchable - Use good keywords and tags
  6. Track Usage - Monitor which help topics are most viewed
  7. Gather Feedback - Allow users to rate help content

Next: Best Practices - Recommended patterns and guidelines