skill-web 0.3.0

Web interface for Skill Engine - built with Yew and WebAssembly
Documentation
//! Wizard state management hook for multi-step execution flow
//!
//! This hook manages the wizard-style navigation through skill execution:
//! 1. Select Skill
//! 2. Select Tool
//! 3. Configure Parameters
//! 4. Execute
//!
//! Features:
//! - Step validation before progression
//! - localStorage persistence (auto-save/restore)
//! - Confirmation before data loss
//! - Step accessibility control

use std::collections::HashMap;
use yew::prelude::*;
use serde::{Deserialize, Serialize};
use gloo_storage::{LocalStorage, Storage};

const WIZARD_STATE_KEY: &str = "skill-web-wizard-state";

/// Wizard step enumeration
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum WizardStep {
    SelectSkill,
    SelectTool,
    ConfigureParameters,
    Execute,
}

impl WizardStep {
    /// Get the next step in sequence
    pub fn next(&self) -> Option<Self> {
        match self {
            Self::SelectSkill => Some(Self::SelectTool),
            Self::SelectTool => Some(Self::ConfigureParameters),
            Self::ConfigureParameters => Some(Self::Execute),
            Self::Execute => None,
        }
    }

    /// Get the previous step in sequence
    pub fn prev(&self) -> Option<Self> {
        match self {
            Self::SelectSkill => None,
            Self::SelectTool => Some(Self::SelectSkill),
            Self::ConfigureParameters => Some(Self::SelectTool),
            Self::Execute => Some(Self::ConfigureParameters),
        }
    }

    /// Get step number (1-indexed for UI display)
    pub fn number(&self) -> usize {
        match self {
            Self::SelectSkill => 1,
            Self::SelectTool => 2,
            Self::ConfigureParameters => 3,
            Self::Execute => 4,
        }
    }

    /// Get step label for display
    pub fn label(&self) -> &'static str {
        match self {
            Self::SelectSkill => "Select Skill",
            Self::SelectTool => "Select Tool",
            Self::ConfigureParameters => "Configure Parameters",
            Self::Execute => "Execute",
        }
    }
}

/// Wizard state structure
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WizardState {
    pub current_step: WizardStep,
    pub selected_skill: Option<String>,
    pub selected_tool: Option<String>,
    pub selected_instance: Option<String>,
    #[serde(skip)]
    pub parameters: HashMap<String, serde_json::Value>,
    pub validation_errors: HashMap<String, String>,
    pub steps_completed: HashMap<WizardStep, bool>,
}

impl Default for WizardState {
    fn default() -> Self {
        Self {
            current_step: WizardStep::SelectSkill,
            selected_skill: None,
            selected_tool: None,
            selected_instance: None,
            parameters: HashMap::new(),
            validation_errors: HashMap::new(),
            steps_completed: HashMap::new(),
        }
    }
}

impl WizardState {
    /// Validate if current step is complete and can progress
    pub fn can_progress(&self) -> bool {
        match self.current_step {
            WizardStep::SelectSkill => self.selected_skill.is_some(),
            WizardStep::SelectTool => self.selected_tool.is_some(),
            WizardStep::ConfigureParameters => self.validation_errors.is_empty(),
            WizardStep::Execute => false, // Can't progress past execute
        }
    }

    /// Check if a step is accessible (can navigate to it)
    pub fn is_step_accessible(&self, step: WizardStep) -> bool {
        match step {
            WizardStep::SelectSkill => true,
            WizardStep::SelectTool => self.selected_skill.is_some(),
            WizardStep::ConfigureParameters => {
                self.selected_skill.is_some() && self.selected_tool.is_some()
            }
            WizardStep::Execute => {
                self.selected_skill.is_some()
                    && self.selected_tool.is_some()
                    && self.validation_errors.is_empty()
            }
        }
    }

    /// Mark current step as completed
    pub fn complete_current_step(&mut self) {
        self.steps_completed.insert(self.current_step, true);
    }
}

/// Wizard state handle with methods for state manipulation
pub struct WizardStateHandle {
    state: UseStateHandle<WizardState>,
}

impl Clone for WizardStateHandle {
    fn clone(&self) -> Self {
        Self {
            state: self.state.clone(),
        }
    }
}

impl WizardStateHandle {
    /// Get current state
    pub fn get(&self) -> WizardState {
        (*self.state).clone()
    }

    /// Advance to next step if valid
    pub fn next(&self) {
        let mut state = (*self.state).clone();

        if !state.can_progress() {
            return;
        }

        if let Some(next_step) = state.current_step.next() {
            state.complete_current_step();
            state.current_step = next_step;
            self.state.set(state.clone());
            Self::persist(&state);
        }
    }

    /// Go back to previous step
    pub fn prev(&self) {
        let mut state = (*self.state).clone();

        if let Some(prev_step) = state.current_step.prev() {
            state.current_step = prev_step;
            self.state.set(state.clone());
            Self::persist(&state);
        }
    }

    /// Jump to a specific step if accessible
    pub fn go_to(&self, step: WizardStep) {
        let state = (*self.state).clone();

        if !state.is_step_accessible(step) {
            return;
        }

        let mut new_state = state;
        new_state.current_step = step;
        self.state.set(new_state.clone());
        Self::persist(&new_state);
    }

    /// Set selected skill and auto-advance
    pub fn set_skill(&self, skill: String) {
        let mut state = (*self.state).clone();

        // Check if changing skill would lose data
        let has_tool = state.selected_tool.is_some();
        let has_params = !state.parameters.is_empty();

        if has_tool || has_params {
            // TODO: Show confirmation dialog in UI layer
            // For now, just proceed
        }

        state.selected_skill = Some(skill);
        state.selected_tool = None; // Clear tool when changing skill
        state.parameters.clear(); // Clear params when changing skill
        state.complete_current_step();

        // Auto-advance to tool selection
        state.current_step = WizardStep::SelectTool;

        self.state.set(state.clone());
        Self::persist(&state);
    }

    /// Set selected tool and auto-advance
    pub fn set_tool(&self, tool: String) {
        let mut state = (*self.state).clone();

        // Check if changing tool would lose params
        let has_params = !state.parameters.is_empty();
        let changing_tool = state.selected_tool.as_ref().map(|t| t != &tool).unwrap_or(false);

        if has_params && changing_tool {
            // TODO: Show confirmation dialog in UI layer
            state.parameters.clear();
        }

        state.selected_tool = Some(tool);
        state.complete_current_step();

        // Auto-advance to parameter configuration
        state.current_step = WizardStep::ConfigureParameters;

        self.state.set(state.clone());
        Self::persist(&state);
    }

    /// Set selected instance
    pub fn set_instance(&self, instance: Option<String>) {
        let mut state = (*self.state).clone();
        state.selected_instance = instance;
        self.state.set(state.clone());
        Self::persist(&state);
    }

    /// Update a parameter value
    pub fn set_parameter(&self, name: String, value: serde_json::Value) {
        let mut state = (*self.state).clone();
        state.parameters.insert(name, value);
        self.state.set(state.clone());
        Self::persist(&state);
    }

    /// Set validation errors
    pub fn set_validation_errors(&self, errors: HashMap<String, String>) {
        let mut state = (*self.state).clone();
        state.validation_errors = errors;
        self.state.set(state.clone());
        // Don't persist validation errors
    }

    /// Reset entire wizard state
    pub fn reset(&self) {
        let state = WizardState::default();
        self.state.set(state.clone());
        Self::persist(&state);
    }

    /// Persist state to localStorage
    fn persist(state: &WizardState) {
        let _ = LocalStorage::set(WIZARD_STATE_KEY, state);
    }

    /// Load state from localStorage
    fn load() -> WizardState {
        LocalStorage::get(WIZARD_STATE_KEY).unwrap_or_default()
    }
}

/// Hook to use wizard state
#[hook]
pub fn use_wizard_state() -> WizardStateHandle {
    // Load from localStorage on first render
    let state = use_state(WizardStateHandle::load);

    WizardStateHandle { state }
}