Coverage for src/commands/wizard/renderer.py: 66%
89 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-06-05 22:56 -0700
« prev ^ index » next coverage.py v7.8.0, created at 2025-06-05 22:56 -0700
1"""
2UI rendering for setup wizard.
3"""
5import platform
6import subprocess
7import logging
8from typing import List, Dict, Any
9from rich.console import Console
10from rich.table import Table
11from rich import box
13from .state import WizardState, WizardStep
14from .steps import SetupStep
16from src.ui.theme import (
17 INFO_STYLE,
18 TABLE_TITLE_STYLE,
19 SUCCESS_STYLE,
20 ERROR_STYLE,
21 WARNING_STYLE,
22)
25class WizardRenderer:
26 """Handles UI rendering for the setup wizard."""
28 def __init__(self, console: Console):
29 self.console = console
31 def clear_terminal(self):
32 """Clear the terminal screen."""
33 system = platform.system().lower()
35 try:
36 if system == "windows":
37 subprocess.run("cls", shell=True, check=False)
38 else:
39 subprocess.run("clear", shell=True, check=False)
40 except Exception as e:
41 logging.debug(f"Failed to clear terminal: {e}")
43 def render_step_header(
44 self, step_number: int, title: str, clear_screen: bool = True
45 ):
46 """Display a header for the current step."""
47 if clear_screen:
48 self.clear_terminal()
49 self.console.print(f"\n[bold]Step {step_number}: {title}[/bold]")
50 self.console.print("=" * 50)
52 def render_error(self, message: str):
53 """Render an error message."""
54 self.console.print(f"[{ERROR_STYLE}]Error: {message}[/{ERROR_STYLE}]")
56 def render_warning(self, message: str):
57 """Render a warning message."""
58 self.console.print(f"[{WARNING_STYLE}]{message}[/{WARNING_STYLE}]")
60 def render_success(self, message: str):
61 """Render a success message."""
62 self.console.print(f"[{SUCCESS_STYLE}]{message}[/{SUCCESS_STYLE}]")
64 def render_info(self, message: str):
65 """Render an info message."""
66 self.console.print(f"[{INFO_STYLE}]{message}[/{INFO_STYLE}]")
68 def render_prompt(self, message: str):
69 """Render a prompt message."""
70 self.console.print(message)
72 def render_step(
73 self,
74 step: "SetupStep",
75 state: WizardState,
76 step_number: int,
77 clear_screen: bool = False,
78 ):
79 """Render a complete step including header and prompt."""
80 # Clear screen first if needed
81 if clear_screen:
82 self.clear_terminal()
84 # Render any error message BEFORE the step header so it's clear the error is from previous step
85 if state.error_message:
86 self.render_error(state.error_message)
87 self.console.print() # Add blank line after error
89 # Render step header (but don't clear screen again since we already did it)
90 self.render_step_header(step_number, step.get_step_title(), clear_screen=False)
92 # Handle special rendering for specific steps
93 if state.current_step == WizardStep.MODEL_SELECTION:
94 self._render_models_list(state.models)
95 elif state.current_step == WizardStep.USAGE_CONSENT:
96 self._render_usage_consent_info()
98 # Render the prompt
99 prompt_message = step.get_prompt_message(state)
100 self.render_prompt(prompt_message)
102 def render_completion(self):
103 """Render wizard completion message."""
104 self.console.print("\n[bold]Setup wizard completed successfully![/bold]")
105 self.console.print("You are now ready to use Chuck with all features enabled.")
106 self.console.print("Type /help to see available commands.")
108 def _render_models_list(self, models: List[Dict[str, Any]]):
109 """Render the list of available models."""
110 if not models:
111 self.render_warning("No models available.")
112 return
114 self.console.print("\nAvailable models:")
116 # Define recommended models
117 recommended_models = [
118 "databricks-meta-llama-3-3-70b-instruct",
119 "databricks-claude-3-7-sonnet",
120 ]
122 # Sort models - recommended first
123 sorted_models = []
125 # Add recommended models first
126 for rec_model in recommended_models:
127 for model in models:
128 if model["name"] == rec_model:
129 sorted_models.append(model)
130 break
132 # Add remaining models
133 for model in models:
134 if model["name"] not in recommended_models:
135 sorted_models.append(model)
137 # Display the models
138 for i, model in enumerate(sorted_models, 1):
139 model_name = model["name"]
140 if model_name in recommended_models:
141 self.console.print(f"{i}. {model_name} [green](recommended)[/green]")
142 else:
143 self.console.print(f"{i}. {model_name}")
145 def _render_usage_consent_info(self):
146 """Render usage consent information."""
147 self.console.print(
148 "\nChuck is a research preview application meant to showcase a new wave of data engineering tooling powered by AI.\n"
149 )
150 self.console.print(
151 "Our goal is to learn as much about what is working and not working as possible, and your usage is key to that!\n"
152 )
153 self.console.print(
154 "Chuck can log usage to Amperity so that we can see how users are using the application. "
155 "This is a key piece of information that we will use to inform our roadmap, prioritize bug fixes, and refine existing features.\n"
156 )
157 self.console.print(
158 "Chuck runs locally and Amperity will never have access to your data.\n"
159 )
161 # Create table showing what Chuck shares vs never shares
162 table = Table(show_header=True, header_style=TABLE_TITLE_STYLE, box=box.SIMPLE)
163 table.add_column("Chuck shares", style="green")
164 table.add_column("Chuck NEVER shares", style="red bold")
166 table.add_row(
167 "1. Prompts you type",
168 "1. Your data or the values in the tables you interact with",
169 )
170 table.add_row("2. Tools/context the LLM uses", "2. Credentials of any form")
171 table.add_row(
172 "3. Errors you encounter",
173 "3. Security details about your Databricks account",
174 )
176 self.console.print(table)
178 self.console.print(
179 "\nChuck is an Open Source CLI and you can always review the code for security at https://github.com/amperity/chuck-data\n"
180 )
182 def get_step_number(self, step: WizardStep) -> int:
183 """Get display step number for a wizard step."""
184 step_numbers = {
185 WizardStep.AMPERITY_AUTH: 1,
186 WizardStep.WORKSPACE_URL: 2,
187 WizardStep.TOKEN_INPUT: 3,
188 WizardStep.MODEL_SELECTION: 4,
189 WizardStep.USAGE_CONSENT: 5,
190 WizardStep.COMPLETE: 6,
191 }
192 return step_numbers.get(step, 1)