skill_web/components/run/
skill_selector.rs

1//! Skill selector component - Step 1 of the execution workflow
2
3use yew::prelude::*;
4use crate::store::skills::SkillSummary;
5
6#[derive(Properties, PartialEq)]
7pub struct SkillSelectorProps {
8    pub skills: Vec<SkillSummary>,
9    pub selected: Option<String>,
10    pub loading: bool,
11    pub on_select: Callback<String>,
12}
13
14#[function_component(SkillSelector)]
15pub fn skill_selector(props: &SkillSelectorProps) -> Html {
16    let max_visible = 5;
17
18    html! {
19        <div class="space-y-4">
20            // Step header
21            <div class="flex items-center gap-2 mb-2">
22                <div class="flex items-center justify-center w-8 h-8 rounded-full bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 font-semibold text-sm">
23                    { "1" }
24                </div>
25                <h3 class="text-lg font-semibold text-gray-900 dark:text-white">
26                    { "Select Skill" }
27                </h3>
28            </div>
29
30            <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
31                <div class="p-4 space-y-3">
32                    if props.loading {
33                        // Loading skeleton
34                        { for (0..3).map(|_| html! {
35                            <div class="animate-pulse">
36                                <div class="h-16 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
37                            </div>
38                        })}
39                    } else if props.skills.is_empty() {
40                        <div class="text-center py-8 text-gray-500">
41                            <svg class="w-12 h-12 mx-auto mb-3 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
42                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
43                            </svg>
44                            <p class="text-sm">{ "No skills available" }</p>
45                        </div>
46                    } else {
47                        // Skill cards
48                        { for props.skills.iter().take(max_visible).map(|skill| {
49                            let is_selected = props.selected.as_ref().map(|s| s == &skill.name).unwrap_or(false);
50                            let skill_name = skill.name.clone();
51                            let on_click = {
52                                let on_select = props.on_select.clone();
53                                Callback::from(move |_| {
54                                    on_select.emit(skill_name.clone());
55                                })
56                            };
57
58                            html! {
59                                <button
60                                    onclick={on_click}
61                                    class={format!(
62                                        "w-full text-left p-3 rounded-lg border-2 transition-all hover:border-primary-300 dark:hover:border-primary-700 {}",
63                                        if is_selected {
64                                            "border-primary-500 bg-primary-50 dark:bg-primary-900/20 dark:border-primary-500"
65                                        } else {
66                                            "border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/30"
67                                        }
68                                    )}
69                                >
70                                    <div class="flex items-start gap-3">
71                                        // Radio button
72                                        <div class={format!(
73                                            "w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0 mt-0.5 transition-colors {}",
74                                            if is_selected {
75                                                "border-primary-500 bg-primary-500"
76                                            } else {
77                                                "border-gray-300 dark:border-gray-600"
78                                            }
79                                        )}>
80                                            if is_selected {
81                                                <div class="w-2.5 h-2.5 bg-white rounded-full"></div>
82                                            }
83                                        </div>
84
85                                        // Skill info
86                                        <div class="flex-1 min-w-0">
87                                            <div class="flex items-center gap-2 flex-wrap">
88                                                <span class="font-medium text-gray-900 dark:text-white">
89                                                    { &skill.name }
90                                                </span>
91                                                <span class={format!(
92                                                    "text-xs px-2 py-0.5 rounded-full font-medium {}",
93                                                    match skill.runtime {
94                                                        crate::store::skills::SkillRuntime::Native => "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300",
95                                                        crate::store::skills::SkillRuntime::Docker => "bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300",
96                                                        _ => "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300"
97                                                    }
98                                                )}>
99                                                    { match skill.runtime {
100                                                        crate::store::skills::SkillRuntime::Native => "native",
101                                                        crate::store::skills::SkillRuntime::Docker => "docker",
102                                                        crate::store::skills::SkillRuntime::Wasm => "wasm",
103                                                    }}
104                                                </span>
105                                            </div>
106                                            <div class="flex items-center gap-4 mt-1 text-xs text-gray-600 dark:text-gray-400">
107                                                <span class="flex items-center gap-1">
108                                                    <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
109                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
110                                                    </svg>
111                                                    { format!("{} tools", skill.tools_count) }
112                                                </span>
113                                                if skill.execution_count > 0 {
114                                                    <span class="flex items-center gap-1">
115                                                        <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
116                                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
117                                                        </svg>
118                                                        { format!("{} runs", skill.execution_count) }
119                                                    </span>
120                                                }
121                                            </div>
122                                            if !skill.description.is_empty() {
123                                                <p class="text-xs text-gray-500 dark:text-gray-500 mt-1.5 line-clamp-2">
124                                                    { &skill.description }
125                                                </p>
126                                            }
127                                        </div>
128                                    </div>
129                                </button>
130                            }
131                        })}
132
133                        if props.skills.len() > max_visible {
134                            <button class="w-full text-center py-2 text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 hover:bg-primary-50 dark:hover:bg-primary-900/10 rounded-lg transition-colors">
135                                { format!("Show {} more skills...", props.skills.len() - max_visible) }
136                            </button>
137                        }
138                    }
139                </div>
140            </div>
141        </div>
142    }
143}