skill_web/components/
import_config_modal.rs

1//! Import Configuration Modal component
2//!
3//! A beautifully designed modal for importing skill configurations from TOML manifests.
4//! Features:
5//! - Multiple example templates with preview
6//! - Code editor with line numbers
7//! - Live validation with detailed feedback
8//! - Drag-and-drop file upload
9//! - Smooth animations and transitions
10
11use std::rc::Rc;
12use wasm_bindgen::prelude::*;
13use wasm_bindgen_futures::spawn_local;
14use web_sys::{File, FileReader, HtmlTextAreaElement};
15use yew::prelude::*;
16use yewdux::prelude::*;
17
18use crate::api::{Api, ParsedSkill};
19use crate::components::use_notifications;
20use crate::store::ui::{ModalType, UiAction, UiStore};
21
22/// Import state
23#[derive(Clone, PartialEq)]
24enum ImportState {
25    Input,
26    Validating,
27    Preview(Vec<ParsedSkill>),
28    Importing,
29    Complete(usize),
30    Error(String),
31}
32
33/// Validation status for live validation
34#[derive(Clone, PartialEq)]
35enum ValidationStatus {
36    None,
37    Checking,
38    Valid(usize),
39    Invalid(String),
40}
41
42/// Example template
43#[derive(Clone, PartialEq)]
44struct ExampleTemplate {
45    id: &'static str,
46    name: &'static str,
47    description: &'static str,
48    badge: &'static str,
49    badge_color: &'static str,
50    content: &'static str,
51}
52
53const EXAMPLE_TEMPLATES: &[ExampleTemplate] = &[
54    ExampleTemplate {
55        id: "quick-start",
56        name: "Quick Start",
57        description: "Working skills you can test now",
58        badge: "Try Now",
59        badge_color: "green",
60        content: r#"# Quick Start - Testable Skills
61# ==============================
62# These skills use local examples that work immediately.
63# Import this to test the UI right away!
64#
65version = "1"
66
67# ─────────────────────────────────────
68# Simple Skill (WASM) - Works Immediately!
69# ─────────────────────────────────────
70# Tools:
71#   - hello(name, greeting) → Greet someone
72#   - echo(message, repeat) → Echo text
73#   - calculate(operation, a, b) → Math
74
75[skills.simple]
76source = "./examples/wasm-skills/simple-skill"
77description = "Simple greeting and utilities - great for testing!"
78
79[skills.simple.instances.default]
80
81# ─────────────────────────────────────
82# GitHub Skill (WASM) - Requires token
83# ─────────────────────────────────────
84# Tools:
85#   - list_repos(org)
86#   - get_repo(owner, repo)
87#   - search_code(query)
88
89[skills.github]
90source = "./examples/wasm-skills/github-skill"
91description = "GitHub API integration"
92
93[skills.github.instances.default]
94config.token = "${GITHUB_TOKEN}"
95
96# ─────────────────────────────────────
97# Docker Management (Native)
98# ─────────────────────────────────────
99# Tools:
100#   - ps() → List containers
101#   - images() → List images
102#   - logs(container) → View logs
103
104[skills.docker-cli]
105source = "./examples/native-skills/docker-skill"
106description = "Docker container management"
107
108[skills.docker-cli.instances.default]
109"#,
110    },
111    ExampleTemplate {
112        id: "docker-runners",
113        name: "Code Runners",
114        description: "Python, Node.js sandboxed",
115        badge: "Docker",
116        badge_color: "purple",
117        content: r#"# Code Runners (Docker Sandboxed)
118# ================================
119# Execute code in isolated containers.
120# Uses example skills from the repository.
121#
122version = "1"
123
124# ─────────────────────────────────────
125# Python Runner
126# ─────────────────────────────────────
127# From: examples/docker-runtime-skills/python-runner
128# Tools:
129#   - run(code) → Execute Python code
130#   - exec(command) → Run shell command
131
132[skills.python-runner]
133source = "./examples/docker-runtime-skills/python-runner"
134runtime = "docker"
135description = "Execute Python scripts in sandbox"
136
137[skills.python-runner.instances.default]
138
139# ─────────────────────────────────────
140# Node.js Runner
141# ─────────────────────────────────────
142# From: examples/docker-runtime-skills/node-runner
143# Tools:
144#   - run(code) → Execute JavaScript
145#   - exec(command) → Run shell command
146
147[skills.node-runner]
148source = "./examples/docker-runtime-skills/node-runner"
149runtime = "docker"
150description = "Execute Node.js scripts in sandbox"
151
152[skills.node-runner.instances.default]
153
154# ─────────────────────────────────────
155# FFmpeg (Media Processing)
156# ─────────────────────────────────────
157# From: examples/docker-runtime-skills/ffmpeg-skill
158
159[skills.ffmpeg]
160source = "./examples/docker-runtime-skills/ffmpeg-skill"
161runtime = "docker"
162description = "Video/audio processing with FFmpeg"
163
164[skills.ffmpeg.instances.default]
165
166# ─────────────────────────────────────
167# ImageMagick (Image Processing)
168# ─────────────────────────────────────
169# From: examples/docker-runtime-skills/imagemagick-skill
170
171[skills.imagemagick]
172source = "./examples/docker-runtime-skills/imagemagick-skill"
173runtime = "docker"
174description = "Image processing with ImageMagick"
175
176[skills.imagemagick.instances.default]
177"#,
178    },
179    ExampleTemplate {
180        id: "devops",
181        name: "DevOps Tools",
182        description: "Docker, Kubernetes CLIs",
183        badge: "Native",
184        badge_color: "blue",
185        content: r#"# DevOps Tools (Native CLI Wrappers)
186# ===================================
187# Native skills wrap system CLI tools with
188# structured parameters and validation.
189#
190version = "1"
191
192# ─────────────────────────────────────
193# Docker Management
194# ─────────────────────────────────────
195# From: examples/native-skills/docker-skill
196# Tools:
197#   - ps(all, format) → List containers
198#   - images(all) → List images
199#   - run(image, name, detach, ports) → Run container
200#   - exec(container, command) → Execute command
201#   - logs(container, tail, follow) → View logs
202#   - stop(container) → Stop container
203#   - rm(container, force) → Remove container
204#   - pull(image) → Pull image
205#   - build(path, tag) → Build image
206
207[skills.docker]
208source = "./examples/native-skills/docker-skill"
209description = "Docker container and image management"
210
211[skills.docker.instances.default]
212# Uses system Docker socket
213
214# ─────────────────────────────────────
215# Kubernetes Management
216# ─────────────────────────────────────
217# From: examples/native-skills/kubernetes-skill
218# Tools:
219#   - get(resource, name, namespace, output)
220#   - describe(resource, name, namespace)
221#   - logs(pod, container, tail, follow)
222#   - exec(pod, command, namespace)
223#   - apply(content, namespace, dry_run)
224#   - delete(resource, name, namespace)
225#   - scale(deployment, replicas, namespace)
226#   - rollout(subcommand, deployment)
227
228[skills.kubernetes]
229source = "./examples/native-skills/kubernetes-skill"
230description = "Kubernetes cluster management"
231
232[skills.kubernetes.instances.default]
233# Uses default kubeconfig
234
235[skills.kubernetes.instances.production]
236config.context = "prod-cluster"
237"#,
238    },
239    ExampleTemplate {
240        id: "databases",
241        name: "Database Clients",
242        description: "PostgreSQL, Redis (Docker)",
243        badge: "Data",
244        badge_color: "amber",
245        content: r#"# Database Clients (Docker)
246# =========================
247# Connect to databases using containerized clients.
248# Network access enabled for connectivity.
249#
250version = "1"
251
252# ─────────────────────────────────────
253# PostgreSQL Client
254# ─────────────────────────────────────
255# From: examples/docker-runtime-skills/postgres-skill
256# Tools:
257#   - query(sql) → Execute SQL query
258#   - exec(command) → Run psql command
259
260[skills.postgres]
261source = "./examples/docker-runtime-skills/postgres-skill"
262runtime = "docker"
263description = "PostgreSQL database client"
264
265[skills.postgres.instances.local]
266config.host = "localhost"
267config.port = "5432"
268config.database = "postgres"
269config.user = "postgres"
270
271[skills.postgres.instances.docker]
272config.host = "host.docker.internal"
273config.port = "5432"
274config.database = "myapp"
275config.user = "postgres"
276
277# ─────────────────────────────────────
278# Redis Client
279# ─────────────────────────────────────
280# From: examples/docker-runtime-skills/redis-skill
281# Tools:
282#   - command(cmd) → Execute Redis command
283#   - get(key) → Get value
284#   - set(key, value) → Set value
285
286[skills.redis]
287source = "./examples/docker-runtime-skills/redis-skill"
288runtime = "docker"
289description = "Redis database client"
290
291[skills.redis.instances.local]
292config.host = "localhost"
293config.port = "6379"
294"#,
295    },
296    ExampleTemplate {
297        id: "wasm-api",
298        name: "API Integrations",
299        description: "GitHub, Slack, AWS skills",
300        badge: "WASM",
301        badge_color: "indigo",
302        content: r#"# API Integration Skills (WASM)
303# =============================
304# Custom skills that integrate with external APIs.
305# Each requires appropriate API keys/tokens.
306#
307version = "1"
308
309# ─────────────────────────────────────
310# GitHub Skill
311# ─────────────────────────────────────
312# From: examples/wasm-skills/github-skill
313# Tools:
314#   - list_repos(org, visibility)
315#   - get_repo(owner, repo)
316#   - create_issue(owner, repo, title, body)
317#   - list_issues(owner, repo, state)
318#   - search_code(query, language)
319
320[skills.github]
321source = "./examples/wasm-skills/github-skill"
322description = "GitHub API integration"
323
324[skills.github.instances.default]
325config.token = "${GITHUB_TOKEN}"
326
327# ─────────────────────────────────────
328# Slack Skill
329# ─────────────────────────────────────
330# From: examples/wasm-skills/slack-skill
331# Tools:
332#   - post_message(channel, text)
333#   - list_channels()
334#   - get_user(user_id)
335
336[skills.slack]
337source = "./examples/wasm-skills/slack-skill"
338description = "Slack messaging integration"
339
340[skills.slack.instances.default]
341config.bot_token = "${SLACK_BOT_TOKEN}"
342
343# ─────────────────────────────────────
344# AWS Skill
345# ─────────────────────────────────────
346# From: examples/wasm-skills/aws-skill
347# Tools:
348#   - s3_list(bucket, prefix)
349#   - s3_get(bucket, key)
350#   - s3_put(bucket, key, content)
351#   - lambda_invoke(function, payload)
352
353[skills.aws]
354source = "./examples/wasm-skills/aws-skill"
355description = "AWS services integration"
356
357[skills.aws.instances.default]
358config.region = "us-east-1"
359config.access_key = "${AWS_ACCESS_KEY_ID}"
360config.secret_key = "${AWS_SECRET_ACCESS_KEY}"
361"#,
362    },
363    ExampleTemplate {
364        id: "complete",
365        name: "Complete Setup",
366        description: "All available example skills",
367        badge: "Full",
368        badge_color: "rose",
369        content: r#"# Complete Skill Setup
370# ====================
371# All example skills from the repository.
372# Import this to get everything at once!
373#
374version = "1"
375
376# ═══════════════════════════════════════
377# WASM SKILLS (Custom Tools)
378# ═══════════════════════════════════════
379
380[skills.simple]
381source = "./examples/wasm-skills/simple-skill"
382description = "Basic greeting and utilities"
383
384[skills.simple.instances.default]
385
386[skills.github]
387source = "./examples/wasm-skills/github-skill"
388description = "GitHub API"
389
390[skills.github.instances.default]
391config.token = "${GITHUB_TOKEN}"
392
393[skills.slack]
394source = "./examples/wasm-skills/slack-skill"
395description = "Slack messaging"
396
397[skills.slack.instances.default]
398config.bot_token = "${SLACK_BOT_TOKEN}"
399
400[skills.aws]
401source = "./examples/wasm-skills/aws-skill"
402description = "AWS services"
403
404[skills.aws.instances.default]
405config.region = "us-east-1"
406
407# ═══════════════════════════════════════
408# DOCKER SKILLS (Sandboxed Execution)
409# ═══════════════════════════════════════
410
411[skills.python-runner]
412source = "./examples/docker-runtime-skills/python-runner"
413runtime = "docker"
414description = "Python sandbox"
415
416[skills.python-runner.instances.default]
417
418[skills.node-runner]
419source = "./examples/docker-runtime-skills/node-runner"
420runtime = "docker"
421description = "Node.js sandbox"
422
423[skills.node-runner.instances.default]
424
425[skills.ffmpeg]
426source = "./examples/docker-runtime-skills/ffmpeg-skill"
427runtime = "docker"
428description = "Video/audio processing"
429
430[skills.ffmpeg.instances.default]
431
432[skills.postgres]
433source = "./examples/docker-runtime-skills/postgres-skill"
434runtime = "docker"
435description = "PostgreSQL client"
436
437[skills.postgres.instances.local]
438config.host = "localhost"
439
440[skills.redis]
441source = "./examples/docker-runtime-skills/redis-skill"
442runtime = "docker"
443description = "Redis client"
444
445[skills.redis.instances.local]
446config.host = "localhost"
447
448# ═══════════════════════════════════════
449# NATIVE SKILLS (CLI Wrappers)
450# ═══════════════════════════════════════
451
452[skills.docker-cli]
453source = "./examples/native-skills/docker-skill"
454description = "Docker management"
455
456[skills.docker-cli.instances.default]
457
458[skills.kubernetes]
459source = "./examples/native-skills/kubernetes-skill"
460description = "Kubernetes management"
461
462[skills.kubernetes.instances.default]
463"#,
464    },
465];
466
467/// Props for the ImportConfigModal component
468#[derive(Properties, PartialEq)]
469pub struct ImportConfigModalProps {
470    #[prop_or_default]
471    pub on_imported: Callback<usize>,
472    #[prop_or_default]
473    pub on_close: Callback<()>,
474}
475
476/// Import Configuration Modal component
477#[function_component(ImportConfigModal)]
478pub fn import_config_modal(props: &ImportConfigModalProps) -> Html {
479    let (ui_store, ui_dispatch) = use_store::<UiStore>();
480    let notifications = use_notifications();
481
482    // State
483    let content = use_state(String::new);
484    let import_state = use_state(|| ImportState::Input);
485    let merge_mode = use_state(|| true);
486    let is_dragging = use_state(|| false);
487    let warnings = use_state(Vec::<String>::new);
488    let validation_status = use_state(|| ValidationStatus::None);
489    let active_tab = use_state(|| "editor"); // "editor" | "templates"
490    let selected_template = use_state(|| Option::<String>::None);
491    let debounce_timer = use_state(|| Option::<i32>::None);
492
493    let api = use_memo((), |_| Rc::new(Api::new()));
494
495    let is_open = ui_store.modal.open
496        && ui_store.modal.modal_type == Some(ModalType::Import);
497
498    // Live validation effect - debounced
499    {
500        let content = content.clone();
501        let validation_status = validation_status.clone();
502        let api = api.clone();
503        let debounce_timer = debounce_timer.clone();
504
505        use_effect_with((*content).clone(), move |content_value| {
506            if let Some(timer_id) = *debounce_timer {
507                let window = web_sys::window().unwrap();
508                window.clear_timeout_with_handle(timer_id);
509            }
510
511            let content_value = content_value.clone();
512            if content_value.trim().is_empty() {
513                validation_status.set(ValidationStatus::None);
514                return;
515            }
516
517            validation_status.set(ValidationStatus::Checking);
518
519            let validation_status = validation_status.clone();
520            let api = api.clone();
521            let debounce_timer = debounce_timer.clone();
522
523            let closure = Closure::wrap(Box::new(move || {
524                let api = api.clone();
525                let validation_status = validation_status.clone();
526                let content_value = content_value.clone();
527
528                spawn_local(async move {
529                    match api.config.validate_manifest(&content_value).await {
530                        Ok(response) => {
531                            if response.valid {
532                                validation_status.set(ValidationStatus::Valid(response.skills.len()));
533                            } else {
534                                let error = response.errors.first()
535                                    .cloned()
536                                    .unwrap_or_else(|| "Invalid manifest".to_string());
537                                validation_status.set(ValidationStatus::Invalid(error));
538                            }
539                        }
540                        Err(e) => {
541                            validation_status.set(ValidationStatus::Invalid(e.to_string()));
542                        }
543                    }
544                });
545            }) as Box<dyn FnMut()>);
546
547            let window = web_sys::window().unwrap();
548            let timer_id = window.set_timeout_with_callback_and_timeout_and_arguments_0(
549                closure.as_ref().unchecked_ref(),
550                400,
551            ).unwrap();
552
553            debounce_timer.set(Some(timer_id));
554            closure.forget();
555        });
556    }
557
558    // Close handler
559    let on_close = {
560        let ui_dispatch = ui_dispatch.clone();
561        let on_close_prop = props.on_close.clone();
562        let import_state = import_state.clone();
563        let content = content.clone();
564        let validation_status = validation_status.clone();
565        let active_tab = active_tab.clone();
566        let selected_template = selected_template.clone();
567        Callback::from(move |_: MouseEvent| {
568            if !matches!(*import_state, ImportState::Validating | ImportState::Importing) {
569                ui_dispatch.apply(UiAction::CloseModal);
570                on_close_prop.emit(());
571                content.set(String::new());
572                validation_status.set(ValidationStatus::None);
573                active_tab.set("editor");
574                selected_template.set(None);
575            }
576        })
577    };
578
579    let on_backdrop_click = {
580        let on_close = on_close.clone();
581        Callback::from(move |e: MouseEvent| {
582            let target = e.target().unwrap();
583            let current_target = e.current_target().unwrap();
584            if target == current_target {
585                on_close.emit(e);
586            }
587        })
588    };
589
590    let on_content_input = {
591        let content = content.clone();
592        let import_state = import_state.clone();
593        Callback::from(move |e: InputEvent| {
594            let textarea: HtmlTextAreaElement = e.target_unchecked_into();
595            content.set(textarea.value());
596            if matches!(*import_state, ImportState::Error(_)) {
597                import_state.set(ImportState::Input);
598            }
599        })
600    };
601
602    let on_dragover = {
603        let is_dragging = is_dragging.clone();
604        Callback::from(move |e: DragEvent| {
605            e.prevent_default();
606            is_dragging.set(true);
607        })
608    };
609
610    let on_dragleave = {
611        let is_dragging = is_dragging.clone();
612        Callback::from(move |e: DragEvent| {
613            e.prevent_default();
614            is_dragging.set(false);
615        })
616    };
617
618    let on_drop = {
619        let content = content.clone();
620        let is_dragging = is_dragging.clone();
621        let active_tab = active_tab.clone();
622        Callback::from(move |e: DragEvent| {
623            e.prevent_default();
624            is_dragging.set(false);
625            active_tab.set("editor");
626
627            if let Some(data_transfer) = e.data_transfer() {
628                if let Some(files) = data_transfer.files() {
629                    if files.length() > 0 {
630                        if let Some(file) = files.get(0) {
631                            read_file_content(file, content.clone());
632                        }
633                    }
634                }
635            }
636        })
637    };
638
639    let on_file_select = {
640        let content = content.clone();
641        let active_tab = active_tab.clone();
642        Callback::from(move |e: Event| {
643            let input: web_sys::HtmlInputElement = e.target_unchecked_into();
644            if let Some(files) = input.files() {
645                if files.length() > 0 {
646                    if let Some(file) = files.get(0) {
647                        read_file_content(file, content.clone());
648                        active_tab.set("editor");
649                    }
650                }
651            }
652        })
653    };
654
655    let on_tab_change = {
656        let active_tab = active_tab.clone();
657        Callback::from(move |tab: &'static str| {
658            active_tab.set(tab);
659        })
660    };
661
662    let on_select_template = {
663        let content = content.clone();
664        let selected_template = selected_template.clone();
665        let active_tab = active_tab.clone();
666        Callback::from(move |(id, template_content): (String, String)| {
667            selected_template.set(Some(id));
668            content.set(template_content);
669            active_tab.set("editor");
670        })
671    };
672
673    let on_validate = {
674        let api = api.clone();
675        let content = content.clone();
676        let import_state = import_state.clone();
677        let warnings = warnings.clone();
678        let notifications = notifications.clone();
679
680        Callback::from(move |_| {
681            let content_value = (*content).clone();
682            if content_value.trim().is_empty() {
683                notifications.error("Empty Content", "Please enter or upload a manifest file");
684                return;
685            }
686
687            import_state.set(ImportState::Validating);
688
689            let api = api.clone();
690            let import_state = import_state.clone();
691            let warnings = warnings.clone();
692            let notifications = notifications.clone();
693
694            spawn_local(async move {
695                match api.config.validate_manifest(&content_value).await {
696                    Ok(response) => {
697                        if response.valid {
698                            warnings.set(response.warnings);
699                            import_state.set(ImportState::Preview(response.skills));
700                        } else {
701                            let error_msg = response.errors.join("\n");
702                            import_state.set(ImportState::Error(error_msg.clone()));
703                            notifications.error("Validation Failed", &error_msg);
704                        }
705                    }
706                    Err(e) => {
707                        let error_msg = e.to_string();
708                        import_state.set(ImportState::Error(error_msg.clone()));
709                        notifications.error("Validation Error", &error_msg);
710                    }
711                }
712            });
713        })
714    };
715
716    let on_import = {
717        let api = api.clone();
718        let content = content.clone();
719        let merge_mode = merge_mode.clone();
720        let import_state = import_state.clone();
721        let notifications = notifications.clone();
722        let on_imported = props.on_imported.clone();
723        let ui_dispatch = ui_dispatch.clone();
724
725        Callback::from(move |_| {
726            let content_value = (*content).clone();
727            let merge = *merge_mode;
728
729            import_state.set(ImportState::Importing);
730
731            let api = api.clone();
732            let import_state = import_state.clone();
733            let notifications = notifications.clone();
734            let on_imported = on_imported.clone();
735            let ui_dispatch = ui_dispatch.clone();
736
737            spawn_local(async move {
738                match api.config.import_manifest(&content_value, merge, true).await {
739                    Ok(response) => {
740                        if response.success {
741                            let count = response.installed_count;
742                            import_state.set(ImportState::Complete(count));
743                            notifications.success(
744                                "Import Complete",
745                                format!("Successfully imported {} skill(s)", count),
746                            );
747                            on_imported.emit(count);
748                            ui_dispatch.apply(UiAction::CloseModal);
749                        } else {
750                            let error_msg = response.errors.join("\n");
751                            import_state.set(ImportState::Error(error_msg.clone()));
752                            notifications.error("Import Failed", &error_msg);
753                        }
754                    }
755                    Err(e) => {
756                        let error_msg = e.to_string();
757                        import_state.set(ImportState::Error(error_msg.clone()));
758                        notifications.error("Import Error", &error_msg);
759                    }
760                }
761            });
762        })
763    };
764
765    let on_back = {
766        let import_state = import_state.clone();
767        Callback::from(move |_| {
768            import_state.set(ImportState::Input);
769        })
770    };
771
772    let on_toggle_merge = {
773        let merge_mode = merge_mode.clone();
774        Callback::from(move |_| {
775            merge_mode.set(!*merge_mode);
776        })
777    };
778
779    let on_clear = {
780        let content = content.clone();
781        let validation_status = validation_status.clone();
782        let import_state = import_state.clone();
783        Callback::from(move |_: MouseEvent| {
784            content.set(String::new());
785            validation_status.set(ValidationStatus::None);
786            import_state.set(ImportState::Input);
787        })
788    };
789
790    let is_loading = matches!(*import_state, ImportState::Validating | ImportState::Importing);
791    let can_validate = !(*content).trim().is_empty() && !is_loading;
792    let is_valid = matches!(*validation_status, ValidationStatus::Valid(_));
793
794    if !is_open {
795        return html! {};
796    }
797
798    html! {
799        <div
800            class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in"
801            onclick={on_backdrop_click}
802        >
803            <div class="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full max-w-5xl max-h-[85vh] flex flex-col overflow-hidden animate-scale-in border border-gray-200 dark:border-gray-700">
804                // Header
805                <div class="relative px-6 py-5 border-b border-gray-100 dark:border-gray-800 bg-gradient-to-r from-gray-50 to-white dark:from-gray-800/50 dark:to-gray-900">
806                    <div class="flex items-start justify-between">
807                        <div class="flex items-center gap-4">
808                            <div class="w-12 h-12 rounded-xl bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center shadow-lg shadow-primary-500/20">
809                                <svg class="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
810                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
811                                </svg>
812                            </div>
813                            <div>
814                                <h2 class="text-xl font-bold text-gray-900 dark:text-white">
815                                    { "Import Configuration" }
816                                </h2>
817                                <p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
818                                    { "Add skills from a TOML manifest file" }
819                                </p>
820                            </div>
821                        </div>
822                        <button
823                            onclick={on_close.clone()}
824                            class="p-2 rounded-lg text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 transition-all"
825                            disabled={is_loading}
826                        >
827                            <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
828                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
829                            </svg>
830                        </button>
831                    </div>
832                </div>
833
834                // Body
835                <div class="flex-1 overflow-hidden flex">
836                    {
837                        match &*import_state {
838                            ImportState::Input | ImportState::Validating | ImportState::Error(_) => {
839                                render_editor_view(
840                                    &content,
841                                    &is_dragging,
842                                    &import_state,
843                                    &validation_status,
844                                    &active_tab,
845                                    &selected_template,
846                                    on_content_input.clone(),
847                                    on_dragover.clone(),
848                                    on_dragleave.clone(),
849                                    on_drop.clone(),
850                                    on_file_select.clone(),
851                                    on_tab_change.clone(),
852                                    on_select_template.clone(),
853                                    on_clear.clone(),
854                                )
855                            }
856                            ImportState::Preview(skills) => {
857                                render_preview_view(
858                                    skills,
859                                    &warnings,
860                                    &merge_mode,
861                                    on_toggle_merge.clone(),
862                                )
863                            }
864                            ImportState::Importing => render_importing_view(),
865                            ImportState::Complete(count) => render_complete_view(*count),
866                        }
867                    }
868                </div>
869
870                // Footer
871                <div class="px-6 py-4 border-t border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/30">
872                    <div class="flex items-center justify-between">
873                        <div class="flex items-center gap-3">
874                            {
875                                match &*validation_status {
876                                    ValidationStatus::None => html! {
877                                        <span class="text-sm text-gray-400 dark:text-gray-500">
878                                            { "Paste or upload a manifest to get started" }
879                                        </span>
880                                    },
881                                    ValidationStatus::Checking => html! {
882                                        <div class="flex items-center gap-2 text-gray-500 dark:text-gray-400">
883                                            <div class="w-4 h-4 border-2 border-gray-300 border-t-primary-500 rounded-full animate-spin"></div>
884                                            <span class="text-sm">{ "Validating..." }</span>
885                                        </div>
886                                    },
887                                    ValidationStatus::Valid(count) => html! {
888                                        <div class="flex items-center gap-2 px-3 py-1.5 rounded-full bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400">
889                                            <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
890                                                <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
891                                            </svg>
892                                            <span class="text-sm font-medium">{ format!("{} skill{} ready", count, if *count == 1 { "" } else { "s" }) }</span>
893                                        </div>
894                                    },
895                                    ValidationStatus::Invalid(err) => html! {
896                                        <div class="flex items-center gap-2 px-3 py-1.5 rounded-full bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 max-w-md">
897                                            <svg class="w-4 h-4 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
898                                                <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
899                                            </svg>
900                                            <span class="text-sm font-medium truncate" title={err.clone()}>{ err }</span>
901                                        </div>
902                                    },
903                                }
904                            }
905                        </div>
906
907                        <div class="flex items-center gap-3">
908                            {
909                                match &*import_state {
910                                    ImportState::Input | ImportState::Validating | ImportState::Error(_) => {
911                                        html! {
912                                            <>
913                                                <button
914                                                    onclick={on_close.clone()}
915                                                    class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
916                                                    disabled={is_loading}
917                                                >
918                                                    { "Cancel" }
919                                                </button>
920                                                <button
921                                                    onclick={on_validate}
922                                                    disabled={!can_validate}
923                                                    class={classes!(
924                                                        "px-5", "py-2.5", "text-sm", "font-semibold", "rounded-xl", "transition-all", "flex", "items-center", "gap-2",
925                                                        if can_validate && is_valid {
926                                                            "bg-gradient-to-r from-green-500 to-emerald-500 text-white shadow-lg shadow-green-500/25 hover:shadow-green-500/40 hover:scale-[1.02]"
927                                                        } else if can_validate {
928                                                            "bg-gradient-to-r from-primary-500 to-primary-600 text-white shadow-lg shadow-primary-500/25 hover:shadow-primary-500/40 hover:scale-[1.02]"
929                                                        } else {
930                                                            "bg-gray-100 dark:bg-gray-800 text-gray-400 cursor-not-allowed"
931                                                        }
932                                                    )}
933                                                >
934                                                    if matches!(*import_state, ImportState::Validating) {
935                                                        <div class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
936                                                        { "Validating..." }
937                                                    } else if is_valid {
938                                                        <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
939                                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
940                                                        </svg>
941                                                        { "Continue to Import" }
942                                                    } else {
943                                                        { "Validate & Continue" }
944                                                    }
945                                                </button>
946                                            </>
947                                        }
948                                    }
949                                    ImportState::Preview(_) => {
950                                        html! {
951                                            <>
952                                                <button
953                                                    onclick={on_back}
954                                                    class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors flex items-center gap-2"
955                                                >
956                                                    <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
957                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
958                                                    </svg>
959                                                    { "Back" }
960                                                </button>
961                                                <button
962                                                    onclick={on_import}
963                                                    class="px-5 py-2.5 text-sm font-semibold rounded-xl bg-gradient-to-r from-primary-500 to-primary-600 text-white shadow-lg shadow-primary-500/25 hover:shadow-primary-500/40 hover:scale-[1.02] transition-all flex items-center gap-2"
964                                                >
965                                                    <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
966                                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
967                                                    </svg>
968                                                    { "Import Skills" }
969                                                </button>
970                                            </>
971                                        }
972                                    }
973                                    ImportState::Importing => {
974                                        html! {
975                                            <button
976                                                disabled={true}
977                                                class="px-5 py-2.5 text-sm font-semibold rounded-xl bg-gray-100 dark:bg-gray-800 text-gray-400 flex items-center gap-2"
978                                            >
979                                                <div class="w-4 h-4 border-2 border-gray-300 border-t-gray-500 rounded-full animate-spin"></div>
980                                                { "Importing..." }
981                                            </button>
982                                        }
983                                    }
984                                    ImportState::Complete(_) => {
985                                        html! {
986                                            <button
987                                                onclick={on_close}
988                                                class="px-5 py-2.5 text-sm font-semibold rounded-xl bg-gradient-to-r from-green-500 to-emerald-500 text-white shadow-lg shadow-green-500/25 hover:shadow-green-500/40 hover:scale-[1.02] transition-all"
989                                            >
990                                                { "Done" }
991                                            </button>
992                                        }
993                                    }
994                                }
995                            }
996                        </div>
997                    </div>
998                </div>
999            </div>
1000        </div>
1001    }
1002}
1003
1004/// Render the editor view with tabs
1005fn render_editor_view(
1006    content: &UseStateHandle<String>,
1007    is_dragging: &UseStateHandle<bool>,
1008    import_state: &UseStateHandle<ImportState>,
1009    _validation_status: &UseStateHandle<ValidationStatus>,
1010    active_tab: &UseStateHandle<&'static str>,
1011    selected_template: &UseStateHandle<Option<String>>,
1012    on_content_input: Callback<InputEvent>,
1013    on_dragover: Callback<DragEvent>,
1014    on_dragleave: Callback<DragEvent>,
1015    on_drop: Callback<DragEvent>,
1016    on_file_select: Callback<Event>,
1017    on_tab_change: Callback<&'static str>,
1018    on_select_template: Callback<(String, String)>,
1019    on_clear: Callback<MouseEvent>,
1020) -> Html {
1021    let tab_editor = {
1022        let on_tab_change = on_tab_change.clone();
1023        Callback::from(move |_: MouseEvent| on_tab_change.emit("editor"))
1024    };
1025
1026    let tab_templates = {
1027        let on_tab_change = on_tab_change.clone();
1028        Callback::from(move |_: MouseEvent| on_tab_change.emit("templates"))
1029    };
1030
1031    html! {
1032        <div class="flex-1 flex flex-col overflow-hidden">
1033            // Tab bar
1034            <div class="flex items-center justify-between px-6 py-3 border-b border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-900">
1035                <div class="flex items-center gap-1 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg">
1036                    <button
1037                        onclick={tab_editor}
1038                        class={classes!(
1039                            "px-4", "py-2", "text-sm", "font-medium", "rounded-md", "transition-all",
1040                            if **active_tab == "editor" {
1041                                "bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm"
1042                            } else {
1043                                "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
1044                            }
1045                        )}
1046                    >
1047                        <span class="flex items-center gap-2">
1048                            <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1049                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
1050                            </svg>
1051                            { "Editor" }
1052                        </span>
1053                    </button>
1054                    <button
1055                        onclick={tab_templates}
1056                        class={classes!(
1057                            "px-4", "py-2", "text-sm", "font-medium", "rounded-md", "transition-all",
1058                            if **active_tab == "templates" {
1059                                "bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm"
1060                            } else {
1061                                "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
1062                            }
1063                        )}
1064                    >
1065                        <span class="flex items-center gap-2">
1066                            <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1067                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
1068                            </svg>
1069                            { "Templates" }
1070                        </span>
1071                    </button>
1072                </div>
1073
1074                <div class="flex items-center gap-2">
1075                    if !(**content).is_empty() {
1076                        <button
1077                            onclick={on_clear}
1078                            class="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
1079                        >
1080                            <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1081                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
1082                            </svg>
1083                            { "Clear" }
1084                        </button>
1085                    }
1086                    <label class="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors cursor-pointer">
1087                        <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1088                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
1089                        </svg>
1090                        { "Upload File" }
1091                        <input
1092                            type="file"
1093                            accept=".toml,.txt"
1094                            class="hidden"
1095                            onchange={on_file_select}
1096                        />
1097                    </label>
1098                </div>
1099            </div>
1100
1101            // Content area
1102            <div class="flex-1 overflow-hidden">
1103                if **active_tab == "editor" {
1104                    { render_code_editor(content, is_dragging, import_state, on_content_input, on_dragover, on_dragleave, on_drop) }
1105                } else {
1106                    { render_templates_view(selected_template, on_select_template) }
1107                }
1108            </div>
1109        </div>
1110    }
1111}
1112
1113/// Render the code editor
1114fn render_code_editor(
1115    content: &UseStateHandle<String>,
1116    is_dragging: &UseStateHandle<bool>,
1117    import_state: &UseStateHandle<ImportState>,
1118    on_content_input: Callback<InputEvent>,
1119    on_dragover: Callback<DragEvent>,
1120    on_dragleave: Callback<DragEvent>,
1121    on_drop: Callback<DragEvent>,
1122) -> Html {
1123    let line_count = if (*content).is_empty() { 20 } else { (*content).lines().count().max(20) };
1124
1125    html! {
1126        <div
1127            class="h-full flex relative"
1128            ondragover={on_dragover}
1129            ondragleave={on_dragleave}
1130            ondrop={on_drop}
1131        >
1132            // Error banner
1133            if let ImportState::Error(error) = &**import_state {
1134                <div class="absolute top-0 left-0 right-0 z-10 m-4">
1135                    <div class="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-xl p-4 shadow-lg">
1136                        <div class="flex items-start gap-3">
1137                            <div class="w-8 h-8 rounded-lg bg-red-100 dark:bg-red-900/50 flex items-center justify-center flex-shrink-0">
1138                                <svg class="w-4 h-4 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1139                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
1140                                </svg>
1141                            </div>
1142                            <div class="flex-1 min-w-0">
1143                                <p class="text-sm font-semibold text-red-800 dark:text-red-200">
1144                                    { "Validation Error" }
1145                                </p>
1146                                <p class="text-sm text-red-600 dark:text-red-400 mt-1 font-mono whitespace-pre-wrap">
1147                                    { error }
1148                                </p>
1149                            </div>
1150                        </div>
1151                    </div>
1152                </div>
1153            }
1154
1155            // Line numbers
1156            <div class="w-14 bg-gray-50 dark:bg-gray-950 border-r border-gray-200 dark:border-gray-800 py-4 overflow-hidden select-none flex-shrink-0">
1157                <div class="space-y-0">
1158                    { for (1..=line_count).map(|n| {
1159                        html! {
1160                            <div class="h-6 leading-6 text-right pr-4 text-xs font-mono text-gray-400 dark:text-gray-600">
1161                                { n }
1162                            </div>
1163                        }
1164                    }) }
1165                </div>
1166            </div>
1167
1168            // Editor
1169            <div class="flex-1 relative">
1170                <textarea
1171                    class="absolute inset-0 w-full h-full p-4 bg-white dark:bg-gray-900 text-sm font-mono text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-600 focus:outline-none resize-none leading-6"
1172                    placeholder="# Paste your .skill-engine.toml content here
1173
1174version = \"1\"
1175
1176[skills.my-skill]
1177source = \"github:username/my-skill\"
1178description = \"My awesome skill\"
1179
1180[skills.my-skill.instances.default]
1181config.api_key = \"${API_KEY}\""
1182                    value={(**content).clone()}
1183                    oninput={on_content_input}
1184                    spellcheck="false"
1185                />
1186            </div>
1187
1188            // Drag overlay
1189            if **is_dragging {
1190                <div class="absolute inset-0 z-20 flex items-center justify-center bg-primary-500/10 dark:bg-primary-500/20 backdrop-blur-sm border-2 border-dashed border-primary-500 rounded-lg m-2">
1191                    <div class="text-center">
1192                        <div class="w-16 h-16 mx-auto rounded-2xl bg-primary-500/20 flex items-center justify-center mb-4">
1193                            <svg class="w-8 h-8 text-primary-600 dark:text-primary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1194                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
1195                            </svg>
1196                        </div>
1197                        <p class="text-lg font-semibold text-primary-700 dark:text-primary-300">
1198                            { "Drop your file here" }
1199                        </p>
1200                        <p class="text-sm text-primary-600/70 dark:text-primary-400/70 mt-1">
1201                            { "Supports .toml and .txt files" }
1202                        </p>
1203                    </div>
1204                </div>
1205            }
1206        </div>
1207    }
1208}
1209
1210/// Render the templates view
1211fn render_templates_view(
1212    selected_template: &UseStateHandle<Option<String>>,
1213    on_select_template: Callback<(String, String)>,
1214) -> Html {
1215    html! {
1216        <div class="h-full overflow-y-auto p-6 bg-gray-50/50 dark:bg-gray-950/50">
1217            <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
1218                { for EXAMPLE_TEMPLATES.iter().map(|template| {
1219                    let is_selected = selected_template.as_ref().map(|s| s.as_str()) == Some(template.id);
1220                    let on_click = on_select_template.clone();
1221                    let id = template.id.to_string();
1222                    let content = template.content.to_string();
1223
1224                    let badge_classes = match template.badge_color {
1225                        "green" => "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400",
1226                        "blue" => "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400",
1227                        "purple" => "bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400",
1228                        "amber" => "bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400",
1229                        "indigo" => "bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-400",
1230                        "rose" => "bg-rose-100 dark:bg-rose-900/30 text-rose-700 dark:text-rose-400",
1231                        _ => "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-400",
1232                    };
1233
1234                    html! {
1235                        <button
1236                            onclick={Callback::from(move |_| on_click.emit((id.clone(), content.clone())))}
1237                            class={classes!(
1238                                "group", "relative", "p-5", "rounded-xl", "border-2", "text-left", "transition-all", "hover:shadow-lg",
1239                                if is_selected {
1240                                    "border-primary-500 bg-primary-50 dark:bg-primary-900/20 shadow-lg shadow-primary-500/10"
1241                                } else {
1242                                    "border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-gray-300 dark:hover:border-gray-600"
1243                                }
1244                            )}
1245                        >
1246                            // Selected indicator
1247                            if is_selected {
1248                                <div class="absolute top-3 right-3">
1249                                    <div class="w-6 h-6 rounded-full bg-primary-500 flex items-center justify-center">
1250                                        <svg class="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1251                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
1252                                        </svg>
1253                                    </div>
1254                                </div>
1255                            }
1256
1257                            <div class="flex items-start gap-4">
1258                                <div class={classes!(
1259                                    "w-12", "h-12", "rounded-xl", "flex", "items-center", "justify-center", "flex-shrink-0", "transition-transform", "group-hover:scale-110",
1260                                    match template.badge_color {
1261                                        "green" => "bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400",
1262                                        "blue" => "bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400",
1263                                        "purple" => "bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400",
1264                                        "amber" => "bg-amber-100 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400",
1265                                        "indigo" => "bg-indigo-100 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400",
1266                                        "rose" => "bg-rose-100 dark:bg-rose-900/30 text-rose-600 dark:text-rose-400",
1267                                        _ => "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400",
1268                                    }
1269                                )}>
1270                                    { render_template_icon(template.id) }
1271                                </div>
1272                                <div class="flex-1 min-w-0">
1273                                    <div class="flex items-center gap-2 mb-1">
1274                                        <h3 class="text-sm font-semibold text-gray-900 dark:text-white">
1275                                            { template.name }
1276                                        </h3>
1277                                        <span class={classes!("text-[10px]", "font-bold", "uppercase", "px-2", "py-0.5", "rounded-full", badge_classes)}>
1278                                            { template.badge }
1279                                        </span>
1280                                    </div>
1281                                    <p class="text-sm text-gray-500 dark:text-gray-400">
1282                                        { template.description }
1283                                    </p>
1284                                </div>
1285                            </div>
1286
1287                            // Preview snippet
1288                            <div class="mt-4 p-3 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700/50">
1289                                <pre class="text-xs font-mono text-gray-600 dark:text-gray-400 overflow-hidden whitespace-pre-wrap line-clamp-3">
1290                                    { template.content.lines().take(4).collect::<Vec<_>>().join("\n") }
1291                                </pre>
1292                            </div>
1293                        </button>
1294                    }
1295                }) }
1296            </div>
1297        </div>
1298    }
1299}
1300
1301/// Render template icon
1302fn render_template_icon(id: &str) -> Html {
1303    match id {
1304        "quick-start" => html! {
1305            // Rocket/play icon
1306            <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1307                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
1308            </svg>
1309        },
1310        "docker-runners" => html! {
1311            // Docker whale icon
1312            <svg class="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
1313                <path d="M13 3h2v2h-2V3zm3 0h2v2h-2V3zm-6 0h2v2H10V3zM7 3h2v2H7V3zm0 3h2v2H7V6zm3 0h2v2h-2V6zm3 0h2v2h-2V6zm3 0h2v2h-2V6zm3 0h2v2h-2V6zM4 9h2v2H4V9zm3 0h2v2H7V9zm3 0h2v2h-2V9zm3 0h2v2h-2V9zm3 0h2v2h-2V9zm3 0h2v2h-2V9zm2.5 3c-.4 0-.8.1-1.2.2-.4-1.2-1.5-2-2.8-2H3c-1.1 0-2 .9-2 2v3c0 2.2 1.8 4 4 4h12c2.8 0 5-2.2 5-5 0-1.4-.6-2.8-1.5-3.8-.5-.3-1-.4-1.5-.4z"/>
1314            </svg>
1315        },
1316        "devops" => html! {
1317            // Terminal/CLI icon
1318            <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1319                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
1320            </svg>
1321        },
1322        "databases" => html! {
1323            // Database icon
1324            <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1325                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
1326            </svg>
1327        },
1328        "wasm-api" => html! {
1329            // API/cloud icon
1330            <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1331                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
1332            </svg>
1333        },
1334        "complete" => html! {
1335            // Stack/all icon
1336            <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1337                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
1338            </svg>
1339        },
1340        _ => html! {
1341            <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1342                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
1343            </svg>
1344        },
1345    }
1346}
1347
1348/// Render the preview view
1349fn render_preview_view(
1350    skills: &[ParsedSkill],
1351    warnings: &UseStateHandle<Vec<String>>,
1352    merge_mode: &UseStateHandle<bool>,
1353    on_toggle_merge: Callback<MouseEvent>,
1354) -> Html {
1355    html! {
1356        <div class="flex-1 overflow-y-auto p-6">
1357            <div class="max-w-2xl mx-auto space-y-6">
1358                // Success banner
1359                <div class="text-center py-6">
1360                    <div class="w-16 h-16 mx-auto rounded-2xl bg-gradient-to-br from-green-400 to-emerald-500 flex items-center justify-center shadow-lg shadow-green-500/30 mb-4">
1361                        <svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1362                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
1363                        </svg>
1364                    </div>
1365                    <h3 class="text-xl font-bold text-gray-900 dark:text-white">
1366                        { "Manifest Validated" }
1367                    </h3>
1368                    <p class="text-gray-500 dark:text-gray-400 mt-1">
1369                        { format!("Found {} skill{} ready to import", skills.len(), if skills.len() == 1 { "" } else { "s" }) }
1370                    </p>
1371                </div>
1372
1373                // Warnings
1374                if !warnings.is_empty() {
1375                    <div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-4">
1376                        <div class="flex items-start gap-3">
1377                            <div class="w-8 h-8 rounded-lg bg-amber-100 dark:bg-amber-900/50 flex items-center justify-center flex-shrink-0">
1378                                <svg class="w-4 h-4 text-amber-600 dark:text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1379                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
1380                                </svg>
1381                            </div>
1382                            <div>
1383                                <p class="text-sm font-semibold text-amber-800 dark:text-amber-200">
1384                                    { "Warnings" }
1385                                </p>
1386                                <ul class="text-sm text-amber-700 dark:text-amber-300 mt-1 space-y-1">
1387                                    { for warnings.iter().map(|w| html! { <li class="flex items-start gap-2"><span class="text-amber-400">{ "•" }</span>{ w }</li> }) }
1388                                </ul>
1389                            </div>
1390                        </div>
1391                    </div>
1392                }
1393
1394                // Merge mode toggle
1395                <div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800/50 rounded-xl border border-gray-200 dark:border-gray-700">
1396                    <div>
1397                        <p class="text-sm font-semibold text-gray-900 dark:text-white">
1398                            { "Merge with existing" }
1399                        </p>
1400                        <p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
1401                            { if **merge_mode { "Keep existing skills, add new ones" } else { "Replace all with imported" } }
1402                        </p>
1403                    </div>
1404                    <button
1405                        type="button"
1406                        role="switch"
1407                        aria-checked={(**merge_mode).to_string()}
1408                        onclick={on_toggle_merge}
1409                        class={classes!(
1410                            "relative", "inline-flex", "h-7", "w-12", "flex-shrink-0",
1411                            "cursor-pointer", "rounded-full", "border-2", "border-transparent",
1412                            "transition-colors", "duration-200", "ease-in-out",
1413                            "focus:outline-none", "focus:ring-2", "focus:ring-primary-500", "focus:ring-offset-2",
1414                            if **merge_mode { "bg-primary-500" } else { "bg-gray-200 dark:bg-gray-600" }
1415                        )}
1416                    >
1417                        <span
1418                            class={classes!(
1419                                "pointer-events-none", "inline-block", "h-6", "w-6",
1420                                "transform", "rounded-full", "bg-white", "shadow-lg",
1421                                "ring-0", "transition", "duration-200", "ease-in-out",
1422                                if **merge_mode { "translate-x-5" } else { "translate-x-0" }
1423                            )}
1424                        />
1425                    </button>
1426                </div>
1427
1428                // Skills list
1429                <div class="space-y-3">
1430                    { for skills.iter().map(|skill| render_skill_card(skill)) }
1431                </div>
1432            </div>
1433        </div>
1434    }
1435}
1436
1437/// Render a skill card
1438fn render_skill_card(skill: &ParsedSkill) -> Html {
1439    let (badge_text, badge_class) = match skill.runtime.as_str() {
1440        "docker" => ("Docker", "bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400"),
1441        "native" => ("Native", "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400"),
1442        _ => ("WASM", "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400"),
1443    };
1444
1445    html! {
1446        <div class="p-4 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow">
1447            <div class="flex items-start justify-between">
1448                <div class="flex items-start gap-3">
1449                    <div class="w-10 h-10 rounded-lg bg-gray-100 dark:bg-gray-700 flex items-center justify-center flex-shrink-0">
1450                        <svg class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1451                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
1452                        </svg>
1453                    </div>
1454                    <div class="min-w-0">
1455                        <div class="flex items-center gap-2">
1456                            <h4 class="text-sm font-semibold text-gray-900 dark:text-white truncate">
1457                                { &skill.name }
1458                            </h4>
1459                            <span class={classes!("text-[10px]", "font-bold", "uppercase", "px-2", "py-0.5", "rounded-full", badge_class)}>
1460                                { badge_text }
1461                            </span>
1462                        </div>
1463                        if let Some(ref desc) = skill.description {
1464                            <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
1465                                { desc }
1466                            </p>
1467                        }
1468                        <p class="text-xs text-gray-400 dark:text-gray-500 mt-1 font-mono">
1469                            { &skill.source }
1470                        </p>
1471                    </div>
1472                </div>
1473                <div class="text-xs text-gray-400 dark:text-gray-500 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">
1474                    { format!("{} inst.", skill.instances.len()) }
1475                </div>
1476            </div>
1477        </div>
1478    }
1479}
1480
1481/// Render the importing view
1482fn render_importing_view() -> Html {
1483    html! {
1484        <div class="flex-1 flex flex-col items-center justify-center p-6">
1485            <div class="relative">
1486                <div class="w-20 h-20 rounded-2xl bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center shadow-xl shadow-primary-500/30">
1487                    <svg class="w-10 h-10 text-white animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1488                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
1489                    </svg>
1490                </div>
1491                <div class="absolute inset-0 rounded-2xl border-4 border-primary-500/30 animate-ping"></div>
1492            </div>
1493            <h3 class="mt-6 text-xl font-bold text-gray-900 dark:text-white">
1494                { "Importing Skills..." }
1495            </h3>
1496            <p class="text-gray-500 dark:text-gray-400 mt-1">
1497                { "This may take a moment" }
1498            </p>
1499        </div>
1500    }
1501}
1502
1503/// Render the complete view
1504fn render_complete_view(count: usize) -> Html {
1505    html! {
1506        <div class="flex-1 flex flex-col items-center justify-center p-6">
1507            <div class="w-20 h-20 rounded-2xl bg-gradient-to-br from-green-400 to-emerald-500 flex items-center justify-center shadow-xl shadow-green-500/30">
1508                <svg class="w-10 h-10 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1509                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
1510                </svg>
1511            </div>
1512            <h3 class="mt-6 text-xl font-bold text-gray-900 dark:text-white">
1513                { "Import Complete!" }
1514            </h3>
1515            <p class="text-gray-500 dark:text-gray-400 mt-1">
1516                { format!("Successfully imported {} skill{}", count, if count == 1 { "" } else { "s" }) }
1517            </p>
1518        </div>
1519    }
1520}
1521
1522/// Read file content using FileReader API
1523fn read_file_content(file: File, content: UseStateHandle<String>) {
1524    let reader = FileReader::new().unwrap();
1525    let reader_clone = reader.clone();
1526
1527    let onload = Closure::wrap(Box::new(move |_: web_sys::Event| {
1528        if let Ok(result) = reader_clone.result() {
1529            if let Some(text) = result.as_string() {
1530                content.set(text);
1531            }
1532        }
1533    }) as Box<dyn FnMut(_)>);
1534
1535    reader.set_onload(Some(onload.as_ref().unchecked_ref()));
1536    reader.read_as_text(&file).unwrap();
1537    onload.forget();
1538}
1539
1540/// Hook to open the import config modal
1541#[hook]
1542pub fn use_import_config_modal() -> UseImportConfigModalHandle {
1543    let (_, dispatch) = use_store::<UiStore>();
1544    UseImportConfigModalHandle { dispatch }
1545}
1546
1547/// Handle for the import config modal hook
1548pub struct UseImportConfigModalHandle {
1549    dispatch: Dispatch<UiStore>,
1550}
1551
1552impl UseImportConfigModalHandle {
1553    pub fn open(&self) {
1554        self.dispatch.apply(UiAction::OpenModal(ModalType::Import, None));
1555    }
1556}
1557
1558impl Clone for UseImportConfigModalHandle {
1559    fn clone(&self) -> Self {
1560        Self {
1561            dispatch: self.dispatch.clone(),
1562        }
1563    }
1564}