1+ import re
12from importlib .resources import files
23
4+ import jinja2
5+ import markdown
36import yaml
7+ from IPython .display import display
8+ from ipywidgets import HTML , Button , HBox , Label , Layout , VBox
9+ from pygments import highlight
10+ from pygments .formatters import HtmlFormatter
11+ from pygments .lexers import PythonLexer
412
5- from scarlet2 .questionnaire .models import Questionnaire
13+ from scarlet2 .questionnaire .models import Question , Questionnaire , Template
614
7- FILE_PACKAGE_PATH = "scarlet2.questionnaire"
8- FILE_NAME = "questions.yaml"
15+ PACKAGE_PATH = "scarlet2.questionnaire"
16+ QUESTIONS_FILE_NAME = "questions.yaml"
17+
18+ VIEWS_PACKAGE_PATH = "scarlet2.questionnaire.views"
19+ OUTPUT_BOX_TEMPLATE_FILE = "output_box.html.jinja"
20+ OUTPUT_BOX_STYLE_FILE = "output_box.css"
21+
22+
23+ QUESTION_BOX_LAYOUT = Layout (
24+ padding = "12px" ,
25+ backgroundColor = "#f9f9f9" ,
26+ border = "1px solid #ddd" ,
27+ borderRadius = "10px" ,
28+ width = "45%" ,
29+ )
30+
31+ OUTPUT_BOX_LAYOUT = Layout (
32+ width = "50%" ,
33+ margin = "0 0 0 20px" ,
34+ )
35+
36+
37+ class QuestionnaireWidget :
38+ """A widget to run an interactive questionnaire in a Jupyter notebook."""
39+
40+ def __init__ (self , questionnaire : Questionnaire ):
41+ self .questions = questionnaire .questions
42+ self .code_output = questionnaire .initial_template
43+ self .commentary = questionnaire .initial_commentary
44+
45+ self ._init_questions ()
46+ self ._init_ui ()
47+
48+ self ._render_output_box ()
49+ self ._render_next_question ()
50+
51+ def _init_questions (self ):
52+ self .current_question = None
53+ self .questions_stack = []
54+ self .question_answers = []
55+ self ._add_questions_to_stack (self .questions )
56+
57+ def _init_ui (self ):
58+ self .output_container = HTML ()
59+ self .question_box = VBox ([], layout = QUESTION_BOX_LAYOUT )
60+ self .output_box = VBox ([self .output_container ], layout = OUTPUT_BOX_LAYOUT )
61+
62+ self .ui = HBox ([self .question_box , self .output_box ])
63+
64+ def _add_questions_to_stack (self , questions : list [Question ]):
65+ self .questions_stack = questions + self .questions_stack
66+
67+ def _render_next_question (self ):
68+ self .current_question = self .questions_stack .pop (0 ) if len (self .questions_stack ) > 0 else None
69+ self ._render_question_box ()
70+
71+ def _render_question_box (self ):
72+ previous_qs = self ._generate_previous_questions ()
73+
74+ if self .current_question is None :
75+ self .question_box .children = previous_qs + [Label ("🎉 You're done!" )]
76+ return
77+
78+ q_label = HTML (f"<b>{ self .current_question .question } </b>" )
79+
80+ buttons = []
81+ for i , answer in enumerate (self .current_question .answers ):
82+ button = Button (
83+ description = answer .answer ,
84+ tooltip = answer .tooltip ,
85+ layout = Layout (width = "auto" , margin = "4px 0" ),
86+ )
87+
88+ def on_click_handler (btn , index = i ):
89+ self ._handle_answer (index )
90+
91+ button .on_click (on_click_handler )
92+
93+ buttons .append (button )
94+
95+ self .question_box .children = previous_qs + [q_label ] + buttons
96+
97+ def _generate_previous_questions (self ):
98+ children = []
99+ for q , ans_ind in self .question_answers :
100+ html_str = f"""
101+ <div style="background_color: #111">
102+ <span style="color: #888; padding-right: 10px;">{ q .question } </span>
103+ <span style="color: #555;">{ q .answers [ans_ind ].answer } </span>
104+ </div>
105+ """
106+ children .append (HTML (html_str ))
107+ return children
108+
109+ def _handle_answer (self , answer_index : int ):
110+ answer = self .current_question .answers [answer_index ]
111+
112+ self ._update_template (answer .templates )
113+ self .commentary = answer .commentary
114+ self ._render_output_box ()
115+
116+ self ._add_questions_to_stack (answer .followups )
117+ self .question_answers .append ((self .current_question , answer_index ))
118+ self ._render_next_question ()
119+
120+ def _update_template (self , templates : list [Template ]):
121+ for t in templates :
122+ pattern = r"\{\{\s*" + re .escape (t .replacement ) + r"\s*\}\}"
123+ self .code_output = re .sub (pattern , t .code , self .code_output )
124+
125+ def _render_output_box (self ):
126+ output_code = re .sub (r"\{\{\s*\w+\s*\}\}" , "" , self .code_output )
127+ commentary_html = markdown .markdown (self .commentary , extensions = ["extra" ])
128+
129+ formatter = HtmlFormatter (style = "monokai" , noclasses = True )
130+ highlighted_code = highlight (output_code , PythonLexer (), formatter )
131+
132+ # Load Jinja template
133+ template_file = files (VIEWS_PACKAGE_PATH ).joinpath (OUTPUT_BOX_TEMPLATE_FILE )
134+ with template_file .open ("r" ) as f :
135+ template_source = f .read ()
136+ template = jinja2 .Template (template_source )
137+
138+ # Load additional CSS for styling
139+ css_file = files (VIEWS_PACKAGE_PATH ).joinpath (OUTPUT_BOX_STYLE_FILE )
140+ with css_file .open ("r" ) as f :
141+ css_content = f .read ()
142+
143+ html_content = f"<style>{ css_content } </style>\n " + template .render (
144+ highlighted_code = highlighted_code ,
145+ raw_code = output_code ,
146+ commentary_html = commentary_html ,
147+ )
148+
149+ self .output_container .value = html_content
150+
151+ def show (self ):
152+ """Display the widget in a Jupyter notebook."""
153+ display (self .ui )
9154
10155
11156def load_questions () -> Questionnaire :
@@ -14,7 +159,22 @@ def load_questions() -> Questionnaire:
14159 Returns:
15160 Questionnaire: The loaded questionnaire model.
16161 """
17- questions_path = files (FILE_PACKAGE_PATH ).joinpath (FILE_NAME )
162+ questions_path = files (PACKAGE_PATH ).joinpath (QUESTIONS_FILE_NAME )
18163 with questions_path .open ("r" ) as f :
19164 raw = yaml .safe_load (f )
20165 return Questionnaire .model_validate (raw )
166+
167+
168+ def run_questionnaire ():
169+ """Run the Scarlet2 initialization questionnaire in a Jupyter notebook.
170+
171+ The questionnaire guides the user through a series of questions to set up
172+ the initialization of a Scarlet2 project that fits their use case.
173+
174+ The user will be presented with questions and multiple-choice answers, and
175+ at the end of the questionnaire, a code snippet that can be used as a
176+ template for initializing Scarlet2 will be generated.
177+ """
178+ questions = load_questions ()
179+ app = QuestionnaireWidget (questions )
180+ app .show ()
0 commit comments