skill_web/pages/
onboarding.rs

1//! Onboarding wizard page
2
3use yew::prelude::*;
4use yew_router::prelude::*;
5
6use crate::components::icons::{CheckIcon, ChevronRightIcon};
7use crate::router::Route;
8
9/// Onboarding page props
10#[derive(Properties, PartialEq)]
11pub struct OnboardingPageProps {
12    #[prop_or("welcome".to_string())]
13    pub step: String,
14}
15
16/// Onboarding page component
17#[function_component(OnboardingPage)]
18pub fn onboarding_page(props: &OnboardingPageProps) -> Html {
19    let steps = vec![
20        ("welcome", "Welcome"),
21        ("search", "Search"),
22        ("credentials", "Credentials"),
23        ("skills", "Skills"),
24        ("complete", "Complete"),
25    ];
26
27    let current_step_idx = steps.iter().position(|(id, _)| *id == props.step).unwrap_or(0);
28
29    html! {
30        <div class="min-h-screen bg-gradient-to-br from-primary-900 to-primary-950 flex flex-col">
31            // Header
32            <header class="p-6">
33                <div class="flex items-center gap-3">
34                    <span class="text-3xl">{ "⚡" }</span>
35                    <span class="text-xl font-semibold text-white">{ "Skill Engine" }</span>
36                </div>
37            </header>
38
39            // Progress indicator
40            <div class="px-6 py-4">
41                <div class="max-w-2xl mx-auto">
42                    <div class="flex items-center justify-between">
43                        { for steps.iter().enumerate().map(|(i, (id, label))| {
44                            let is_complete = i < current_step_idx;
45                            let is_current = i == current_step_idx;
46
47                            html! {
48                                <>
49                                    <div class="flex flex-col items-center">
50                                        <div class={classes!(
51                                            "w-10", "h-10", "rounded-full", "flex", "items-center", "justify-center", "font-medium", "transition-colors",
52                                            if is_complete {
53                                                "bg-success-500 text-white"
54                                            } else if is_current {
55                                                "bg-white text-primary-900"
56                                            } else {
57                                                "bg-primary-800 text-primary-400"
58                                            }
59                                        )}>
60                                            if is_complete {
61                                                <CheckIcon class="w-5 h-5" />
62                                            } else {
63                                                { (i + 1).to_string() }
64                                            }
65                                        </div>
66                                        <span class={classes!(
67                                            "mt-2", "text-xs", "font-medium",
68                                            if is_current { "text-white" } else { "text-primary-400" }
69                                        )}>
70                                            { *label }
71                                        </span>
72                                    </div>
73                                    if i < steps.len() - 1 {
74                                        <div class={classes!(
75                                            "flex-1", "h-1", "mx-2", "rounded",
76                                            if is_complete { "bg-success-500" } else { "bg-primary-800" }
77                                        )} />
78                                    }
79                                </>
80                            }
81                        }) }
82                    </div>
83                </div>
84            </div>
85
86            // Content area
87            <main class="flex-1 flex items-center justify-center p-6">
88                <div class="w-full max-w-2xl">
89                    {
90                        match props.step.as_str() {
91                            "welcome" => html! { <WelcomeStep /> },
92                            "search" => html! { <SearchStep /> },
93                            "credentials" => html! { <CredentialsStep /> },
94                            "skills" => html! { <SkillsStep /> },
95                            "complete" => html! { <CompleteStep /> },
96                            _ => html! { <WelcomeStep /> },
97                        }
98                    }
99                </div>
100            </main>
101        </div>
102    }
103}
104
105/// Welcome step component
106#[function_component(WelcomeStep)]
107fn welcome_step() -> Html {
108    let navigator = use_navigator().unwrap();
109
110    let on_start = {
111        let navigator = navigator.clone();
112        Callback::from(move |_| {
113            navigator.push(&Route::OnboardingStep { step: "search".to_string() });
114        })
115    };
116
117    let on_skip = {
118        let navigator = navigator.clone();
119        Callback::from(move |_| {
120            navigator.push(&Route::Dashboard);
121        })
122    };
123
124    html! {
125        <div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8 text-center">
126            <div class="text-6xl mb-6">{ "⚡" }</div>
127            <h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">
128                { "Welcome to Skill Engine" }
129            </h1>
130            <p class="text-lg text-gray-600 dark:text-gray-300 mb-8 max-w-md mx-auto">
131                { "Give your AI agents superpowers with sandboxed WASM skill execution" }
132            </p>
133
134            <div class="space-y-3 text-left max-w-sm mx-auto mb-8">
135                { for [
136                    "Search Pipeline - How skills are discovered",
137                    "AI Integration - Connect LLM providers",
138                    "Starter Skills - Get productive immediately",
139                    "Claude Code - Seamless integration",
140                ].iter().map(|item| html! {
141                    <div class="flex items-center gap-3">
142                        <CheckIcon class="w-5 h-5 text-success-500 flex-shrink-0" />
143                        <span class="text-gray-700 dark:text-gray-300">{ *item }</span>
144                    </div>
145                }) }
146            </div>
147
148            <p class="text-sm text-gray-500 mb-6">
149                { "Estimated time: 3-5 minutes" }
150            </p>
151
152            <div class="flex flex-col gap-3">
153                <button class="btn btn-primary w-full justify-center" onclick={on_start}>
154                    { "Get Started" }
155                    <ChevronRightIcon class="w-4 h-4 ml-2" />
156                </button>
157                <button class="btn btn-ghost w-full justify-center text-gray-500" onclick={on_skip}>
158                    { "Skip to Dashboard" }
159                </button>
160            </div>
161        </div>
162    }
163}
164
165/// Search setup step component
166#[function_component(SearchStep)]
167fn search_step() -> Html {
168    let navigator = use_navigator().unwrap();
169    let embedding_provider = use_state(|| "fastembed".to_string());
170    let vector_store = use_state(|| "inmemory".to_string());
171
172    let on_next = {
173        let navigator = navigator.clone();
174        Callback::from(move |_| {
175            navigator.push(&Route::OnboardingStep { step: "credentials".to_string() });
176        })
177    };
178
179    let on_back = {
180        let navigator = navigator.clone();
181        Callback::from(move |_| {
182            navigator.push(&Route::OnboardingStep { step: "welcome".to_string() });
183        })
184    };
185
186    html! {
187        <div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8">
188            <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
189                { "Search Pipeline" }
190            </h2>
191            <p class="text-gray-600 dark:text-gray-300 mb-8">
192                { "Choose how skills are discovered and searched" }
193            </p>
194
195            <div class="space-y-6">
196                // Embedding provider
197                <div>
198                    <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
199                        { "Embedding Provider" }
200                    </label>
201                    <div class="space-y-3">
202                        { for [
203                            ("fastembed", "FastEmbed (Recommended)", "Local, offline, no API keys required"),
204                            ("openai", "OpenAI", "Cloud-based, requires API key"),
205                            ("ollama", "Ollama", "Self-hosted, requires Ollama installation"),
206                        ].iter().map(|(value, label, desc)| {
207                            let is_selected = *embedding_provider == *value;
208                            let provider = embedding_provider.clone();
209                            let val = value.to_string();
210                            let onclick = Callback::from(move |_| provider.set(val.clone()));
211
212                            html! {
213                                <label class={classes!(
214                                    "flex", "items-start", "gap-4", "p-4", "rounded-lg", "border", "cursor-pointer", "transition-colors",
215                                    if is_selected {
216                                        "border-primary-500 bg-primary-50 dark:bg-primary-900/30"
217                                    } else {
218                                        "border-gray-200 dark:border-gray-700 hover:border-gray-300"
219                                    }
220                                )}>
221                                    <input
222                                        type="radio"
223                                        name="embedding"
224                                        value={*value}
225                                        checked={is_selected}
226                                        onclick={onclick}
227                                        class="mt-1"
228                                    />
229                                    <div>
230                                        <span class="font-medium text-gray-900 dark:text-white">{ *label }</span>
231                                        <p class="text-sm text-gray-500 mt-1">{ *desc }</p>
232                                    </div>
233                                </label>
234                            }
235                        }) }
236                    </div>
237                </div>
238
239                // Vector store
240                <div>
241                    <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
242                        { "Vector Store" }
243                    </label>
244                    <div class="space-y-3">
245                        { for [
246                            ("inmemory", "In-Memory (Recommended)", "Great for development and small deployments"),
247                            ("qdrant", "Qdrant", "Production-ready, requires Qdrant server"),
248                        ].iter().map(|(value, label, desc)| {
249                            let is_selected = *vector_store == *value;
250                            let store = vector_store.clone();
251                            let val = value.to_string();
252                            let onclick = Callback::from(move |_| store.set(val.clone()));
253
254                            html! {
255                                <label class={classes!(
256                                    "flex", "items-start", "gap-4", "p-4", "rounded-lg", "border", "cursor-pointer", "transition-colors",
257                                    if is_selected {
258                                        "border-primary-500 bg-primary-50 dark:bg-primary-900/30"
259                                    } else {
260                                        "border-gray-200 dark:border-gray-700 hover:border-gray-300"
261                                    }
262                                )}>
263                                    <input
264                                        type="radio"
265                                        name="store"
266                                        value={*value}
267                                        checked={is_selected}
268                                        onclick={onclick}
269                                        class="mt-1"
270                                    />
271                                    <div>
272                                        <span class="font-medium text-gray-900 dark:text-white">{ *label }</span>
273                                        <p class="text-sm text-gray-500 mt-1">{ *desc }</p>
274                                    </div>
275                                </label>
276                            }
277                        }) }
278                    </div>
279                </div>
280            </div>
281
282            <div class="flex justify-between mt-8">
283                <button class="btn btn-ghost" onclick={on_back}>{ "Back" }</button>
284                <button class="btn btn-primary" onclick={on_next}>
285                    { "Next" }
286                    <ChevronRightIcon class="w-4 h-4 ml-2" />
287                </button>
288            </div>
289        </div>
290    }
291}
292
293/// Credentials step component
294#[function_component(CredentialsStep)]
295fn credentials_step() -> Html {
296    let navigator = use_navigator().unwrap();
297
298    let on_next = {
299        let navigator = navigator.clone();
300        Callback::from(move |_| {
301            navigator.push(&Route::OnboardingStep { step: "skills".to_string() });
302        })
303    };
304
305    let on_back = {
306        let navigator = navigator.clone();
307        Callback::from(move |_| {
308            navigator.push(&Route::OnboardingStep { step: "search".to_string() });
309        })
310    };
311
312    html! {
313        <div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8">
314            <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
315                { "Credentials" }
316            </h2>
317            <p class="text-gray-600 dark:text-gray-300 mb-8">
318                { "Add API keys for enhanced features (optional)" }
319            </p>
320
321            <div class="space-y-4">
322                <div>
323                    <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
324                        { "OpenAI API Key" }
325                        <span class="text-gray-400 ml-1">{ "(optional)" }</span>
326                    </label>
327                    <input
328                        type="password"
329                        class="input"
330                        placeholder="sk-..."
331                    />
332                    <p class="text-xs text-gray-500 mt-1">
333                        { "Used for OpenAI embeddings and skill enhancement" }
334                    </p>
335                </div>
336
337                <div>
338                    <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
339                        { "Anthropic API Key" }
340                        <span class="text-gray-400 ml-1">{ "(optional)" }</span>
341                    </label>
342                    <input
343                        type="password"
344                        class="input"
345                        placeholder="sk-ant-..."
346                    />
347                    <p class="text-xs text-gray-500 mt-1">
348                        { "Used for AI-powered skill enhancement" }
349                    </p>
350                </div>
351            </div>
352
353            <div class="flex justify-between mt-8">
354                <button class="btn btn-ghost" onclick={on_back}>{ "Back" }</button>
355                <button class="btn btn-primary" onclick={on_next}>
356                    { "Next" }
357                    <ChevronRightIcon class="w-4 h-4 ml-2" />
358                </button>
359            </div>
360        </div>
361    }
362}
363
364/// Skills step component
365#[function_component(SkillsStep)]
366fn skills_step() -> Html {
367    let navigator = use_navigator().unwrap();
368
369    let on_next = {
370        let navigator = navigator.clone();
371        Callback::from(move |_| {
372            navigator.push(&Route::OnboardingStep { step: "complete".to_string() });
373        })
374    };
375
376    let on_back = {
377        let navigator = navigator.clone();
378        Callback::from(move |_| {
379            navigator.push(&Route::OnboardingStep { step: "credentials".to_string() });
380        })
381    };
382
383    let starter_skills = vec![
384        ("kubernetes", "Kubernetes cluster management", true),
385        ("github", "GitHub repository operations", true),
386        ("docker", "Docker container management", false),
387        ("aws", "AWS cloud services", false),
388    ];
389
390    html! {
391        <div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8">
392            <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
393                { "Starter Skills" }
394            </h2>
395            <p class="text-gray-600 dark:text-gray-300 mb-8">
396                { "Select skills to install (you can add more later)" }
397            </p>
398
399            <div class="space-y-3">
400                { for starter_skills.iter().map(|(name, desc, default)| {
401                    html! {
402                        <label class="flex items-start gap-4 p-4 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 cursor-pointer transition-colors">
403                            <input
404                                type="checkbox"
405                                checked={*default}
406                                class="mt-1 rounded border-gray-300"
407                            />
408                            <div>
409                                <span class="font-medium text-gray-900 dark:text-white">{ *name }</span>
410                                <p class="text-sm text-gray-500 mt-1">{ *desc }</p>
411                            </div>
412                        </label>
413                    }
414                }) }
415            </div>
416
417            <div class="flex justify-between mt-8">
418                <button class="btn btn-ghost" onclick={on_back}>{ "Back" }</button>
419                <button class="btn btn-primary" onclick={on_next}>
420                    { "Install & Continue" }
421                    <ChevronRightIcon class="w-4 h-4 ml-2" />
422                </button>
423            </div>
424        </div>
425    }
426}
427
428/// Complete step component
429#[function_component(CompleteStep)]
430fn complete_step() -> Html {
431    let navigator = use_navigator().unwrap();
432
433    let on_finish = {
434        let navigator = navigator.clone();
435        Callback::from(move |_| {
436            navigator.push(&Route::Dashboard);
437        })
438    };
439
440    html! {
441        <div class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8 text-center">
442            <div class="w-16 h-16 bg-success-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-6">
443                <CheckIcon class="w-8 h-8 text-success-600 dark:text-green-400" />
444            </div>
445            <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
446                { "You're all set!" }
447            </h2>
448            <p class="text-gray-600 dark:text-gray-300 mb-8">
449                { "Skill Engine is configured and ready to use" }
450            </p>
451
452            <div class="space-y-2 text-left max-w-sm mx-auto mb-8">
453                <div class="flex items-center gap-3">
454                    <CheckIcon class="w-5 h-5 text-success-500" />
455                    <span class="text-gray-700 dark:text-gray-300">{ "Search pipeline configured" }</span>
456                </div>
457                <div class="flex items-center gap-3">
458                    <CheckIcon class="w-5 h-5 text-success-500" />
459                    <span class="text-gray-700 dark:text-gray-300">{ "2 skills installed" }</span>
460                </div>
461                <div class="flex items-center gap-3">
462                    <CheckIcon class="w-5 h-5 text-success-500" />
463                    <span class="text-gray-700 dark:text-gray-300">{ "Ready for Claude Code integration" }</span>
464                </div>
465            </div>
466
467            <button class="btn btn-primary w-full justify-center" onclick={on_finish}>
468                { "Go to Dashboard" }
469                <ChevronRightIcon class="w-4 h-4 ml-2" />
470            </button>
471        </div>
472    }
473}