Skip to main content

rustyclaw_tui/components/
user_prompt_dialog.rs

1// ── User prompt dialog — agent asks user a question ─────────────────────────
2
3use crate::theme;
4use iocraft::prelude::*;
5use rustyclaw_core::user_prompt_types::PromptType;
6
7#[derive(Default, Props)]
8pub struct UserPromptDialogProps {
9    /// The question title from the agent.
10    pub title: String,
11    /// Optional longer description.
12    pub description: String,
13    /// Current user input text (for text input types).
14    pub input: String,
15    /// Selected option index (for Select/MultiSelect).
16    pub selected: usize,
17    /// The prompt type (serialized for prop passing).
18    pub prompt_type: Option<PromptType>,
19}
20
21#[component]
22pub fn UserPromptDialog(props: &UserPromptDialogProps) -> impl Into<AnyElement<'static>> {
23    let has_desc = !props.description.is_empty();
24    let cursor = "▏";
25
26    // Determine what kind of input to show
27    let prompt_type = props.prompt_type.clone().unwrap_or(PromptType::TextInput {
28        placeholder: None,
29        default: None,
30    });
31
32    element! {
33        View(
34            width: 100pct,
35            height: 100pct,
36            justify_content: JustifyContent::Center,
37            align_items: AlignItems::Center,
38        ) {
39            View(
40                width: 70,
41                flex_direction: FlexDirection::Column,
42                border_style: BorderStyle::Round,
43                border_color: theme::ACCENT_BRIGHT,
44                background_color: theme::BG_SURFACE,
45                padding_left: 2,
46                padding_right: 2,
47                padding_top: 1,
48                padding_bottom: 1,
49            ) {
50                // Title
51                Text(
52                    content: "❓ Agent Question",
53                    color: theme::ACCENT_BRIGHT,
54                    weight: Weight::Bold,
55                )
56
57                View(height: 1)
58
59                // Question text
60                Text(
61                    content: props.title.clone(),
62                    color: theme::TEXT,
63                    weight: Weight::Bold,
64                    wrap: TextWrap::Wrap,
65                )
66
67                // Description (if any)
68                #(if has_desc {
69                    element! {
70                        View(margin_top: 1) {
71                            Text(
72                                content: props.description.clone(),
73                                color: theme::MUTED,
74                                wrap: TextWrap::Wrap,
75                            )
76                        }
77                    }.into_any()
78                } else {
79                    element! { View() }.into_any()
80                })
81
82                View(height: 1)
83
84                // Render input based on prompt type
85                #(match &prompt_type {
86                    PromptType::Select { options, .. } => {
87                        element! {
88                            View(flex_direction: FlexDirection::Column) {
89                                Text(content: "Select an option (↑/↓ to navigate, Enter to select):", color: theme::MUTED)
90                                View(height: 1)
91                                #(options.iter().enumerate().map(|(i, opt)| {
92                                    let is_selected = i == props.selected;
93                                    let prefix = if is_selected { "▶ " } else { "  " };
94                                    let fg = if is_selected { theme::ACCENT_BRIGHT } else { theme::TEXT };
95                                    element! {
96                                        View(key: i as u64, flex_direction: FlexDirection::Column) {
97                                            Text(
98                                                content: format!("{}{}", prefix, opt.label),
99                                                color: fg,
100                                                weight: if is_selected { Weight::Bold } else { Weight::Normal },
101                                            )
102                                            #(if let Some(ref desc) = opt.description {
103                                                element! {
104                                                    Text(
105                                                        content: format!("    {}", desc),
106                                                        color: theme::MUTED,
107                                                    )
108                                                }.into_any()
109                                            } else {
110                                                element! { View() }.into_any()
111                                            })
112                                        }
113                                    }
114                                }))
115                            }
116                        }.into_any()
117                    }
118                    PromptType::Confirm { default: _ } => {
119                        let yes_selected = props.selected == 0;
120                        element! {
121                            View(flex_direction: FlexDirection::Row, gap: 2u32) {
122                                Text(
123                                    content: if yes_selected { "▶ Yes" } else { "  Yes" },
124                                    color: if yes_selected { theme::SUCCESS } else { theme::TEXT },
125                                    weight: if yes_selected { Weight::Bold } else { Weight::Normal },
126                                )
127                                Text(
128                                    content: if !yes_selected { "▶ No" } else { "  No" },
129                                    color: if !yes_selected { theme::ERROR } else { theme::TEXT },
130                                    weight: if !yes_selected { Weight::Bold } else { Weight::Normal },
131                                )
132                            }
133                        }.into_any()
134                    }
135                    PromptType::TextInput { placeholder, .. } => {
136                        let placeholder_text = placeholder.clone();
137                        let display = if props.input.is_empty() {
138                            placeholder_text.unwrap_or_default()
139                        } else {
140                            props.input.clone()
141                        };
142                        let is_placeholder = props.input.is_empty();
143                        element! {
144                            View(flex_direction: FlexDirection::Column) {
145                                Text(content: "Your answer:", color: theme::MUTED)
146                                View(
147                                    flex_direction: FlexDirection::Row,
148                                    border_style: BorderStyle::Single,
149                                    border_color: theme::ACCENT,
150                                    padding_left: 1,
151                                    padding_right: 1,
152                                    min_height: 1u32,
153                                ) {
154                                    Text(
155                                        content: format!("{}{}", display, cursor),
156                                        color: if is_placeholder { theme::MUTED } else { theme::TEXT },
157                                    )
158                                }
159                            }
160                        }.into_any()
161                    }
162                    PromptType::Form { .. } => {
163                        // Form: show generic text input for now
164                        element! {
165                            View(flex_direction: FlexDirection::Column) {
166                                Text(content: "Your answer:", color: theme::MUTED)
167                                View(
168                                    flex_direction: FlexDirection::Row,
169                                    border_style: BorderStyle::Single,
170                                    border_color: theme::ACCENT,
171                                    padding_left: 1,
172                                    padding_right: 1,
173                                    min_height: 1u32,
174                                ) {
175                                    Text(
176                                        content: format!("{}{}", props.input, cursor),
177                                        color: theme::TEXT,
178                                    )
179                                }
180                            }
181                        }.into_any()
182                    }
183                    PromptType::MultiSelect { options, .. } => {
184                        // For multi-select, we'd need a Vec<bool> for checked state
185                        // For now, render as single-select
186                        element! {
187                            View(flex_direction: FlexDirection::Column) {
188                                Text(content: "Select options (Space to toggle, Enter to confirm):", color: theme::MUTED)
189                                View(height: 1)
190                                #(options.iter().enumerate().map(|(i, opt)| {
191                                    let is_selected = i == props.selected;
192                                    let prefix = if is_selected { "▶ [ ] " } else { "  [ ] " };
193                                    let fg = if is_selected { theme::ACCENT_BRIGHT } else { theme::TEXT };
194                                    element! {
195                                        View(key: i as u64) {
196                                            Text(
197                                                content: format!("{}{}", prefix, opt.label),
198                                                color: fg,
199                                            )
200                                        }
201                                    }
202                                }))
203                            }
204                        }.into_any()
205                    }
206                })
207
208                View(height: 1)
209
210                // Hint based on prompt type
211                #(match &prompt_type {
212                    PromptType::Select { .. } => {
213                        element! {
214                            Text(content: "↑↓ navigate  ·  Enter ↩ select  ·  Esc dismiss", color: theme::MUTED)
215                        }.into_any()
216                    }
217                    PromptType::Confirm { .. } => {
218                        element! {
219                            Text(content: "←→ or Y/N  ·  Enter ↩ confirm  ·  Esc dismiss", color: theme::MUTED)
220                        }.into_any()
221                    }
222                    _ => {
223                        element! {
224                            Text(content: "Enter ↩ submit  ·  Esc dismiss", color: theme::MUTED)
225                        }.into_any()
226                    }
227                })
228            }
229        }
230    }
231}