1use yew::prelude::*;
4use yew_router::prelude::*;
5
6use crate::components::icons::{CheckIcon, ChevronRightIcon};
7use crate::router::Route;
8
9#[derive(Properties, PartialEq)]
11pub struct OnboardingPageProps {
12 #[prop_or("welcome".to_string())]
13 pub step: String,
14}
15
16#[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 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 <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 <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#[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#[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 <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 <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#[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#[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#[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}