import io import re from reportlab.lib import colors from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Flowable from reportlab.lib.units import mm, inch from reportlab.graphics.shapes import Drawing, Rect, String, Line class SliderFlowable(Flowable): def __init__(self, name, value, min_val, max_val, is_percentage=False): Flowable.__init__(self) self.name = name self.value = value self.min_val = min_val self.max_val = max_val self.is_percentage = is_percentage self.width = 400 self.height = 80 def draw(self): drawing = Drawing(self.width, self.height) # Draw slider bar bar = Rect(50, 30, 300, 20, fillColor=colors.HexColor("#f7fbfd"), strokeColor=colors.HexColor("#9999ff")) drawing.add(bar) # Draw slider value if self.max_val == self.min_val: value_width = 50 # or some default width else: value_width = 50 + ((self.value - self.min_val) / (self.max_val - self.min_val) * 300) value_bar = Rect(50, 30, value_width - 50, 20, fillColor=colors.HexColor("#9999ff"), strokeColor=None) drawing.add(value_bar) # Add slider name drawing.add(String(0, 60, self.name, fontSize=12, fillColor=colors.HexColor("#26004d"))) # Add range labels min_str = f"{self.min_val:.1f}%" if self.is_percentage else f"{self.min_val:.1f}" max_str = f"{self.max_val:.1f}%" if self.is_percentage else f"{self.max_val:.1f}" drawing.add(String(40, 10, min_str, fontSize=10, fillColor=colors.HexColor("#26004d"))) drawing.add(String(340, 10, max_str, fontSize=10, fillColor=colors.HexColor("#26004d"))) # Add value label value_str = f"{self.value:.1f}%" if self.is_percentage else f"{self.value:.1f}" drawing.add(String(value_width - 20, 55, value_str, fontSize=10, fillColor=colors.HexColor("#26004d"))) # Add value marker drawing.add(Line(value_width, 25, value_width, 55, strokeColor=colors.HexColor("#26004d"), strokeWidth=2)) drawing.drawOn(self.canv, 0, 0) def create_styles(): styles = getSampleStyleSheet() styles['Title'].fontName = 'Helvetica-Bold' styles['Title'].fontSize = 18 styles['Title'].spaceAfter = 16 styles['Title'].textColor = colors.HexColor("#26004d") styles['Heading1'].fontName = 'Helvetica-Bold' styles['Heading1'].fontSize = 16 styles['Heading1'].spaceAfter = 10 styles['Heading1'].textColor = colors.HexColor("#3b0b75") styles['Heading2'].fontName = 'Helvetica' styles['Heading2'].fontSize = 14 styles['Heading2'].spaceAfter = 12 styles['Heading2'].textColor = colors.HexColor("#52176a") styles['BodyText'].fontName = 'Helvetica' styles['BodyText'].fontSize = 12 styles['BodyText'].spaceAfter = 12 styles['BodyText'].textColor = colors.HexColor("#26004d") styles.add(ParagraphStyle( name='Answer', parent=styles['BodyText'], backColor=colors.HexColor("#f0f2fd"), borderColor=colors.HexColor("#9999ff"), borderWidth=0.5, borderPadding=(5, 5, 5, 5), spaceAfter=10 )) return styles def create_page_template(canvas, doc): canvas.saveState() canvas.setFillColor(colors.HexColor("#e6ebfb")) canvas.rect(0, 0, doc.pagesize[0], doc.pagesize[1], fill=1) canvas.setFillColor(colors.HexColor("#26004d")) canvas.setFont('Helvetica', 9) canvas.drawString(30, 20, f"Page {doc.page}") canvas.restoreState() def generate_pdf(session_state): buffer = io.BytesIO() doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=20*mm, leftMargin=20*mm, topMargin=20*mm, bottomMargin=20*mm) styles = create_styles() story = [Paragraph("Experimental Validation Method Plan", styles['Title'])] for page in session_state.pages[:-1]: # Skip the last page if 'input_key' in page and page['input_key'] is not None: story.append(Paragraph(page['title'], styles['Heading1'])) if page['input_key'] == 'idea_testing': story.extend(process_idea_testing(session_state, styles)) elif page['input_key'] == 'capability_testing': story.extend(process_capability_testing(session_state, styles)) elif page['input_key'] == 'approach_comparison': story.extend(process_approach_comparison(session_state, styles)) elif page['input_key'] == 'mock_application': story.extend(process_mock_application(session_state, styles)) story.append(Spacer(1, 12)) doc.build(story, onFirstPage=create_page_template, onLaterPages=create_page_template) buffer.seek(0) return buffer def process_idea_testing(session_state, styles): story = [] answers = session_state.answers['idea_testing'] story.append(Paragraph("Rapid Prototyping", styles['Heading2'])) for field in ['input', 'process', 'output']: story.append(Paragraph(f"{field.capitalize()}:", styles['Heading2'])) story.append(Paragraph(answers['rapid_prototyping'].get(field, ""), styles['Answer'])) story.append(Paragraph("Idea Testing", styles['Heading2'])) story.append(Paragraph("Framework for idea validation:", styles['Heading2'])) story.append(Paragraph(answers.get('framework', ""), styles['Answer'])) story.append(Paragraph("Useful libraries, tools, or assets:", styles['Heading2'])) story.append(Paragraph(answers.get('tools', ""), styles['Answer'])) return story def process_capability_testing(session_state, styles): story = [] answers = session_state.answers['capability_testing'] story.append(Paragraph("Capability Testing", styles['Heading2'])) for field in ['capability', 'assessment_method', 'success_definition']: story.append(Paragraph(f"{field.replace('_', ' ').capitalize()}:", styles['Heading2'])) story.append(Paragraph(answers.get(field, ""), styles['Answer'])) story.append(Paragraph("Validation Criteria", styles['Heading2'])) story.extend(process_validation_criteria(answers['validation_criteria'], styles)) return story def process_validation_criteria(session_state, styles): story = [] # Qualitative Criteria story.append(Paragraph("Qualitative Criteria:", styles['Heading2'])) for i, criterion in enumerate(session_state.get('qualitative', [])): description = session_state.get(f'qual_desc_{i}', '') story.append(Paragraph(f"{criterion}: {description}", styles['Answer'])) # Quantitative Criteria story.append(Paragraph("Quantitative Criteria:", styles['Heading2'])) for i, criterion in enumerate(session_state.get('quantitative', [])): parsed = parse_quantitative_criteria(criterion) if parsed: name, min_val, max_val, is_percentage, is_integer = parsed value = session_state.get(f'quant_value_{i}', min_val) if is_percentage: slider = SliderFlowable(name, value*100, min_val*100, max_val*100, is_percentage=True) else: slider = SliderFlowable(name, value, min_val, max_val, is_percentage=False) story.append(slider) story.append(Paragraph(f"{name}: {value:.2f}", styles['Answer'])) return story def process_approach_comparison(session_state, styles): story = [] answers = session_state.answers['approach_comparison'] for field in ['standardization', 'experiment_overview']: story.append(Paragraph(f"{field.replace('_', ' ').capitalize()}:", styles['Heading2'])) story.append(Paragraph(answers.get(field, ""), styles['Answer'])) return story def process_mock_application(session_state, styles): story = [] answers = session_state.answers['mock_application'] for field in ['user_testing', 'insights', 'presentation']: story.append(Paragraph(f"{field.replace('_', ' ').capitalize()}:", styles['Heading2'])) story.append(Paragraph(answers.get(field, ""), styles['Answer'])) return story def parse_quantitative_criteria(input_string): match = re.match(r'(.+)\[([-+]?(?:\d*\.*\d+)(?:%)?)\s*-\s*([-+]?(?:\d*\.*\d+)(?:%)?)?\]', input_string) if match: name, min_val, max_val = match.groups() name = name.strip() # Handle percentage inputs is_percentage = '%' in min_val or '%' in max_val min_val = float(min_val.rstrip('%')) max_val = float(max_val.rstrip('%')) if is_percentage: min_val /= 100 max_val /= 100 is_integer = '.' not in input_string or (min_val.is_integer() and max_val.is_integer()) return name, min_val, max_val, is_percentage, is_integer return None