steer_tui/tui/widgets/setup/
authentication.rs1use crate::tui::state::SetupState;
2use crate::tui::theme::{Component, Theme};
3use ratatui::{
4 buffer::Buffer,
5 layout::{Alignment, Constraint, Direction, Layout, Rect},
6 text::{Line, Span},
7 widgets::{Block, Borders, Paragraph, Widget, Wrap},
8};
9use steer_grpc::client_api::{AuthProgress, ProviderId, provider};
10
11pub struct AuthenticationWidget;
12
13impl AuthenticationWidget {
14 pub fn render(
15 area: Rect,
16 buf: &mut Buffer,
17 state: &SetupState,
18 provider_id: ProviderId,
19 theme: &Theme,
20 ) {
21 let chunks = Layout::default()
22 .direction(Direction::Vertical)
23 .constraints([
24 Constraint::Length(4), Constraint::Min(10), Constraint::Length(3), Constraint::Length(3), ])
29 .split(area);
30
31 let provider_config = state.registry.get(&provider_id);
33 let provider_name = provider_config.map_or("Unknown Provider", |c| c.name.as_str());
34 let is_openai = provider_id == provider::openai();
35
36 let header = vec![
37 Line::from(""),
38 Line::from(Span::styled(
39 format!("Authenticate with {provider_name}"),
40 theme.style(Component::SetupHeader),
41 )),
42 ];
43
44 Paragraph::new(header)
45 .block(
46 Block::default()
47 .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
48 .border_style(theme.style(Component::SetupBorder)),
49 )
50 .alignment(Alignment::Center)
51 .render(chunks[0], buf);
52
53 let mut content = vec![];
55
56 match state.auth_progress.as_ref() {
57 Some(AuthProgress::OAuthStarted { auth_url }) => {
58 content.push(Line::from(""));
59 content.push(Line::from(Span::styled(
60 "OAuth Authentication",
61 theme.style(Component::SetupHeader),
62 )));
63 content.push(Line::from(""));
64 content.push(Line::from("Please visit this URL in your browser:"));
65 content.push(Line::from(""));
66 content.push(Line::from(Span::styled(
67 auth_url,
68 theme.style(Component::SetupUrl),
69 )));
70 content.push(Line::from(""));
71 if is_openai {
72 content.push(Line::from(
73 "After authorizing, you'll be redirected to http://localhost:1455/auth/callback.",
74 ));
75 content.push(Line::from(
76 "If nothing happens, copy the full URL from your browser",
77 ));
78 content.push(Line::from("and paste it below:"));
79 } else {
80 content.push(Line::from(
81 "After authorizing, you'll be redirected to a page showing a code.",
82 ));
83 content.push(Line::from(
84 "Copy the full URL or the code (including the part after the #)",
85 ));
86 content.push(Line::from("and paste it below:"));
87 }
88 content.push(Line::from(""));
89 content.push(Line::from(vec![
90 Span::styled("Callback: ", theme.style(Component::SetupInputLabel)),
91 Span::styled(
92 if state.auth_input.is_empty() {
93 "_"
94 } else {
95 &state.auth_input
96 },
97 theme.style(Component::SetupInputValue),
98 ),
99 ]));
100 }
101 Some(AuthProgress::NeedInput { prompt }) => {
102 content.push(Line::from(""));
103 content.push(Line::from(prompt.clone()));
104 content.push(Line::from(""));
105
106 let masked_input = if state.auth_input.is_empty() {
107 String::from("_")
108 } else {
109 "*".repeat(state.auth_input.len())
110 };
111
112 content.push(Line::from(vec![
113 Span::styled("Input: ", theme.style(Component::SetupInputLabel)),
114 Span::styled(masked_input, theme.style(Component::SetupInputValue)),
115 ]));
116
117 if provider_id == provider::anthropic() {
118 content.push(Line::from(""));
119 content.push(Line::from(Span::styled(
120 "Tip: Get your API key from console.anthropic.com",
121 theme.style(Component::SetupHint),
122 )));
123 }
124 }
125 Some(AuthProgress::InProgress { message }) => {
126 content.push(Line::from(""));
127 content.push(Line::from(message.clone()));
128 }
129 Some(AuthProgress::Complete) => {
130 content.push(Line::from(""));
131 content.push(Line::from("Authentication complete."));
132 }
133 Some(AuthProgress::Error { message }) => {
134 content.push(Line::from(""));
135 content.push(Line::from(format!("Error: {message}")));
136 }
137 None => {
138 content.push(Line::from(""));
139 content.push(Line::from("Starting authentication..."));
140 }
141 }
142
143 Paragraph::new(content)
144 .block(
145 Block::default()
146 .borders(Borders::LEFT | Borders::RIGHT)
147 .border_style(theme.style(Component::SetupBorder)),
148 )
149 .alignment(Alignment::Left)
150 .wrap(Wrap { trim: true })
151 .render(chunks[1], buf);
152
153 if let Some(error) = &state.error_message {
155 let error_text = vec![Line::from(Span::styled(
156 format!("Error: {error}"),
157 theme.style(Component::SetupErrorMessage),
158 ))];
159
160 Paragraph::new(error_text)
161 .block(
162 Block::default()
163 .borders(Borders::LEFT | Borders::RIGHT)
164 .border_style(theme.style(Component::SetupBorder)),
165 )
166 .alignment(Alignment::Center)
167 .render(chunks[2], buf);
168 }
169
170 let instructions = match state.auth_progress.as_ref() {
172 Some(AuthProgress::OAuthStarted { .. } | AuthProgress::NeedInput { .. }) => {
173 vec![Line::from(vec![
174 Span::raw("Type or paste input, "),
175 Span::styled("Enter", theme.style(Component::SetupKeyBinding)),
176 Span::raw(" to submit, "),
177 Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
178 Span::raw(" to cancel"),
179 ])]
180 }
181 _ => vec![Line::from(vec![
182 Span::styled("Esc", theme.style(Component::SetupKeyBinding)),
183 Span::raw(" to go back"),
184 ])],
185 };
186
187 Paragraph::new(instructions)
188 .block(
189 Block::default()
190 .borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT)
191 .border_style(theme.style(Component::SetupBorder)),
192 )
193 .alignment(Alignment::Center)
194 .render(chunks[3], buf);
195 }
196}