skill_web/hooks/
use_wizard_state.rs

1//! Wizard state management hook for multi-step execution flow
2//!
3//! This hook manages the wizard-style navigation through skill execution:
4//! 1. Select Skill
5//! 2. Select Tool
6//! 3. Configure Parameters
7//! 4. Execute
8//!
9//! Features:
10//! - Step validation before progression
11//! - localStorage persistence (auto-save/restore)
12//! - Confirmation before data loss
13//! - Step accessibility control
14
15use std::collections::HashMap;
16use yew::prelude::*;
17use serde::{Deserialize, Serialize};
18use gloo_storage::{LocalStorage, Storage};
19
20const WIZARD_STATE_KEY: &str = "skill-web-wizard-state";
21
22/// Wizard step enumeration
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub enum WizardStep {
25    SelectSkill,
26    SelectTool,
27    ConfigureParameters,
28    Execute,
29}
30
31impl WizardStep {
32    /// Get the next step in sequence
33    pub fn next(&self) -> Option<Self> {
34        match self {
35            Self::SelectSkill => Some(Self::SelectTool),
36            Self::SelectTool => Some(Self::ConfigureParameters),
37            Self::ConfigureParameters => Some(Self::Execute),
38            Self::Execute => None,
39        }
40    }
41
42    /// Get the previous step in sequence
43    pub fn prev(&self) -> Option<Self> {
44        match self {
45            Self::SelectSkill => None,
46            Self::SelectTool => Some(Self::SelectSkill),
47            Self::ConfigureParameters => Some(Self::SelectTool),
48            Self::Execute => Some(Self::ConfigureParameters),
49        }
50    }
51
52    /// Get step number (1-indexed for UI display)
53    pub fn number(&self) -> usize {
54        match self {
55            Self::SelectSkill => 1,
56            Self::SelectTool => 2,
57            Self::ConfigureParameters => 3,
58            Self::Execute => 4,
59        }
60    }
61
62    /// Get step label for display
63    pub fn label(&self) -> &'static str {
64        match self {
65            Self::SelectSkill => "Select Skill",
66            Self::SelectTool => "Select Tool",
67            Self::ConfigureParameters => "Configure Parameters",
68            Self::Execute => "Execute",
69        }
70    }
71}
72
73/// Wizard state structure
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct WizardState {
76    pub current_step: WizardStep,
77    pub selected_skill: Option<String>,
78    pub selected_tool: Option<String>,
79    pub selected_instance: Option<String>,
80    #[serde(skip)]
81    pub parameters: HashMap<String, serde_json::Value>,
82    pub validation_errors: HashMap<String, String>,
83    pub steps_completed: HashMap<WizardStep, bool>,
84}
85
86impl Default for WizardState {
87    fn default() -> Self {
88        Self {
89            current_step: WizardStep::SelectSkill,
90            selected_skill: None,
91            selected_tool: None,
92            selected_instance: None,
93            parameters: HashMap::new(),
94            validation_errors: HashMap::new(),
95            steps_completed: HashMap::new(),
96        }
97    }
98}
99
100impl WizardState {
101    /// Validate if current step is complete and can progress
102    pub fn can_progress(&self) -> bool {
103        match self.current_step {
104            WizardStep::SelectSkill => self.selected_skill.is_some(),
105            WizardStep::SelectTool => self.selected_tool.is_some(),
106            WizardStep::ConfigureParameters => self.validation_errors.is_empty(),
107            WizardStep::Execute => false, // Can't progress past execute
108        }
109    }
110
111    /// Check if a step is accessible (can navigate to it)
112    pub fn is_step_accessible(&self, step: WizardStep) -> bool {
113        match step {
114            WizardStep::SelectSkill => true,
115            WizardStep::SelectTool => self.selected_skill.is_some(),
116            WizardStep::ConfigureParameters => {
117                self.selected_skill.is_some() && self.selected_tool.is_some()
118            }
119            WizardStep::Execute => {
120                self.selected_skill.is_some()
121                    && self.selected_tool.is_some()
122                    && self.validation_errors.is_empty()
123            }
124        }
125    }
126
127    /// Mark current step as completed
128    pub fn complete_current_step(&mut self) {
129        self.steps_completed.insert(self.current_step, true);
130    }
131}
132
133/// Wizard state handle with methods for state manipulation
134pub struct WizardStateHandle {
135    state: UseStateHandle<WizardState>,
136}
137
138impl Clone for WizardStateHandle {
139    fn clone(&self) -> Self {
140        Self {
141            state: self.state.clone(),
142        }
143    }
144}
145
146impl WizardStateHandle {
147    /// Get current state
148    pub fn get(&self) -> WizardState {
149        (*self.state).clone()
150    }
151
152    /// Advance to next step if valid
153    pub fn next(&self) {
154        let mut state = (*self.state).clone();
155
156        if !state.can_progress() {
157            return;
158        }
159
160        if let Some(next_step) = state.current_step.next() {
161            state.complete_current_step();
162            state.current_step = next_step;
163            self.state.set(state.clone());
164            Self::persist(&state);
165        }
166    }
167
168    /// Go back to previous step
169    pub fn prev(&self) {
170        let mut state = (*self.state).clone();
171
172        if let Some(prev_step) = state.current_step.prev() {
173            state.current_step = prev_step;
174            self.state.set(state.clone());
175            Self::persist(&state);
176        }
177    }
178
179    /// Jump to a specific step if accessible
180    pub fn go_to(&self, step: WizardStep) {
181        let state = (*self.state).clone();
182
183        if !state.is_step_accessible(step) {
184            return;
185        }
186
187        let mut new_state = state;
188        new_state.current_step = step;
189        self.state.set(new_state.clone());
190        Self::persist(&new_state);
191    }
192
193    /// Set selected skill and auto-advance
194    pub fn set_skill(&self, skill: String) {
195        let mut state = (*self.state).clone();
196
197        // Check if changing skill would lose data
198        let has_tool = state.selected_tool.is_some();
199        let has_params = !state.parameters.is_empty();
200
201        if has_tool || has_params {
202            // TODO: Show confirmation dialog in UI layer
203            // For now, just proceed
204        }
205
206        state.selected_skill = Some(skill);
207        state.selected_tool = None; // Clear tool when changing skill
208        state.parameters.clear(); // Clear params when changing skill
209        state.complete_current_step();
210
211        // Auto-advance to tool selection
212        state.current_step = WizardStep::SelectTool;
213
214        self.state.set(state.clone());
215        Self::persist(&state);
216    }
217
218    /// Set selected tool and auto-advance
219    pub fn set_tool(&self, tool: String) {
220        let mut state = (*self.state).clone();
221
222        // Check if changing tool would lose params
223        let has_params = !state.parameters.is_empty();
224        let changing_tool = state.selected_tool.as_ref().map(|t| t != &tool).unwrap_or(false);
225
226        if has_params && changing_tool {
227            // TODO: Show confirmation dialog in UI layer
228            state.parameters.clear();
229        }
230
231        state.selected_tool = Some(tool);
232        state.complete_current_step();
233
234        // Auto-advance to parameter configuration
235        state.current_step = WizardStep::ConfigureParameters;
236
237        self.state.set(state.clone());
238        Self::persist(&state);
239    }
240
241    /// Set selected instance
242    pub fn set_instance(&self, instance: Option<String>) {
243        let mut state = (*self.state).clone();
244        state.selected_instance = instance;
245        self.state.set(state.clone());
246        Self::persist(&state);
247    }
248
249    /// Update a parameter value
250    pub fn set_parameter(&self, name: String, value: serde_json::Value) {
251        let mut state = (*self.state).clone();
252        state.parameters.insert(name, value);
253        self.state.set(state.clone());
254        Self::persist(&state);
255    }
256
257    /// Set validation errors
258    pub fn set_validation_errors(&self, errors: HashMap<String, String>) {
259        let mut state = (*self.state).clone();
260        state.validation_errors = errors;
261        self.state.set(state.clone());
262        // Don't persist validation errors
263    }
264
265    /// Reset entire wizard state
266    pub fn reset(&self) {
267        let state = WizardState::default();
268        self.state.set(state.clone());
269        Self::persist(&state);
270    }
271
272    /// Persist state to localStorage
273    fn persist(state: &WizardState) {
274        let _ = LocalStorage::set(WIZARD_STATE_KEY, state);
275    }
276
277    /// Load state from localStorage
278    fn load() -> WizardState {
279        LocalStorage::get(WIZARD_STATE_KEY).unwrap_or_default()
280    }
281}
282
283/// Hook to use wizard state
284#[hook]
285pub fn use_wizard_state() -> WizardStateHandle {
286    // Load from localStorage on first render
287    let state = use_state(WizardStateHandle::load);
288
289    WizardStateHandle { state }
290}