skill_web/store/
skills.rs

1//! Skills state store
2
3use serde::{Deserialize, Serialize};
4use yewdux::prelude::*;
5
6/// Skill status
7#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
8pub enum SkillStatus {
9    Configured,
10    Unconfigured,
11    Error,
12    Loading,
13}
14
15/// Runtime type
16#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
17pub enum SkillRuntime {
18    #[default]
19    Wasm,
20    Docker,
21    Native,
22}
23
24/// Skill summary data
25#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
26pub struct SkillSummary {
27    pub name: String,
28    pub version: String,
29    pub description: String,
30    pub source: String,
31    pub runtime: SkillRuntime,
32    pub tools_count: usize,
33    pub instances_count: usize,
34    pub status: SkillStatus,
35    pub last_used: Option<String>,
36    pub execution_count: u64,
37}
38
39/// Tool information
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41pub struct ToolInfo {
42    pub name: String,
43    pub description: String,
44    pub parameters: Vec<ParameterInfo>,
45    pub streaming: bool,
46}
47
48/// Parameter information
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
50pub struct ParameterInfo {
51    pub name: String,
52    pub param_type: String,
53    pub description: String,
54    pub required: bool,
55    pub default_value: Option<String>,
56}
57
58/// Instance information
59#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
60pub struct InstanceInfo {
61    pub name: String,
62    pub description: Option<String>,
63    pub is_default: bool,
64    pub config_keys: Vec<String>,
65}
66
67/// Detailed skill information
68#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
69pub struct SkillDetail {
70    pub summary: SkillSummary,
71    pub full_description: Option<String>,
72    pub author: Option<String>,
73    pub repository: Option<String>,
74    pub license: Option<String>,
75    pub tools: Vec<ToolInfo>,
76    pub instances: Vec<InstanceInfo>,
77}
78
79/// Skills store state
80#[derive(Clone, Debug, Default, PartialEq, Store)]
81pub struct SkillsStore {
82    /// List of installed skills
83    pub skills: Vec<SkillSummary>,
84    /// Currently selected skill (for detail view)
85    pub selected_skill: Option<SkillDetail>,
86    /// Whether skills are being loaded
87    pub loading: bool,
88    /// Whether a skill detail is being loaded
89    pub detail_loading: bool,
90    /// Error message if loading failed
91    pub error: Option<String>,
92    /// Search query
93    pub search_query: String,
94    /// Selected filters
95    pub status_filter: Option<SkillStatus>,
96    pub source_filter: Option<String>,
97    pub runtime_filter: Option<SkillRuntime>,
98    /// Sort order
99    pub sort_by: SkillSortBy,
100    pub sort_ascending: bool,
101}
102
103/// Sort options for skills
104#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
105pub enum SkillSortBy {
106    #[default]
107    Name,
108    LastUsed,
109    ExecutionCount,
110    ToolsCount,
111}
112
113impl SkillsStore {
114    /// Get filtered and sorted skills based on current search and filters
115    pub fn filtered_skills(&self) -> Vec<&SkillSummary> {
116        let mut skills: Vec<&SkillSummary> = self.skills
117            .iter()
118            .filter(|skill| {
119                // Search filter
120                if !self.search_query.is_empty() {
121                    let query = self.search_query.to_lowercase();
122                    if !skill.name.to_lowercase().contains(&query)
123                        && !skill.description.to_lowercase().contains(&query)
124                    {
125                        return false;
126                    }
127                }
128
129                // Status filter
130                if let Some(ref status) = self.status_filter {
131                    if &skill.status != status {
132                        return false;
133                    }
134                }
135
136                // Source filter
137                if let Some(ref source) = self.source_filter {
138                    if !skill.source.contains(source) {
139                        return false;
140                    }
141                }
142
143                // Runtime filter
144                if let Some(ref runtime) = self.runtime_filter {
145                    if &skill.runtime != runtime {
146                        return false;
147                    }
148                }
149
150                true
151            })
152            .collect();
153
154        // Sort
155        skills.sort_by(|a, b| {
156            let cmp = match self.sort_by {
157                SkillSortBy::Name => a.name.cmp(&b.name),
158                SkillSortBy::LastUsed => a.last_used.cmp(&b.last_used),
159                SkillSortBy::ExecutionCount => a.execution_count.cmp(&b.execution_count),
160                SkillSortBy::ToolsCount => a.tools_count.cmp(&b.tools_count),
161            };
162            if self.sort_ascending { cmp } else { cmp.reverse() }
163        });
164
165        skills
166    }
167
168    /// Get skill by name
169    pub fn get_skill(&self, name: &str) -> Option<&SkillSummary> {
170        self.skills.iter().find(|s| s.name == name)
171    }
172
173    /// Get total skills count
174    pub fn total_count(&self) -> usize {
175        self.skills.len()
176    }
177
178    /// Get count of filtered skills
179    pub fn filtered_count(&self) -> usize {
180        self.filtered_skills().len()
181    }
182}
183
184/// Skills store actions
185pub enum SkillsAction {
186    SetSkills(Vec<SkillSummary>),
187    AddSkill(SkillSummary),
188    RemoveSkill(String),
189    UpdateSkill(SkillSummary),
190    SetSelectedSkill(Option<SkillDetail>),
191    SetLoading(bool),
192    SetDetailLoading(bool),
193    SetError(Option<String>),
194    SetSearchQuery(String),
195    SetStatusFilter(Option<SkillStatus>),
196    SetSourceFilter(Option<String>),
197    SetRuntimeFilter(Option<SkillRuntime>),
198    SetSortBy(SkillSortBy),
199    ToggleSortOrder,
200    ClearFilters,
201}
202
203impl Reducer<SkillsStore> for SkillsAction {
204    fn apply(self, mut store: std::rc::Rc<SkillsStore>) -> std::rc::Rc<SkillsStore> {
205        let state = std::rc::Rc::make_mut(&mut store);
206
207        match self {
208            SkillsAction::SetSkills(skills) => {
209                state.skills = skills;
210                state.loading = false;
211                state.error = None;
212            }
213            SkillsAction::AddSkill(skill) => {
214                // Remove existing if present, then add
215                state.skills.retain(|s| s.name != skill.name);
216                state.skills.push(skill);
217            }
218            SkillsAction::RemoveSkill(name) => {
219                state.skills.retain(|s| s.name != name);
220                // Clear selected if it was removed
221                if let Some(ref selected) = state.selected_skill {
222                    if selected.summary.name == name {
223                        state.selected_skill = None;
224                    }
225                }
226            }
227            SkillsAction::UpdateSkill(skill) => {
228                if let Some(existing) = state.skills.iter_mut().find(|s| s.name == skill.name) {
229                    *existing = skill;
230                }
231            }
232            SkillsAction::SetSelectedSkill(skill) => {
233                state.selected_skill = skill;
234                state.detail_loading = false;
235            }
236            SkillsAction::SetLoading(loading) => {
237                state.loading = loading;
238            }
239            SkillsAction::SetDetailLoading(loading) => {
240                state.detail_loading = loading;
241            }
242            SkillsAction::SetError(error) => {
243                state.error = error;
244                state.loading = false;
245                state.detail_loading = false;
246            }
247            SkillsAction::SetSearchQuery(query) => {
248                state.search_query = query;
249            }
250            SkillsAction::SetStatusFilter(filter) => {
251                state.status_filter = filter;
252            }
253            SkillsAction::SetSourceFilter(filter) => {
254                state.source_filter = filter;
255            }
256            SkillsAction::SetRuntimeFilter(filter) => {
257                state.runtime_filter = filter;
258            }
259            SkillsAction::SetSortBy(sort_by) => {
260                state.sort_by = sort_by;
261            }
262            SkillsAction::ToggleSortOrder => {
263                state.sort_ascending = !state.sort_ascending;
264            }
265            SkillsAction::ClearFilters => {
266                state.search_query = String::new();
267                state.status_filter = None;
268                state.source_filter = None;
269                state.runtime_filter = None;
270            }
271        }
272
273        store
274    }
275}