import os import sys import gradio as gr sys.path.append(os.path.dirname(os.path.dirname(__file__))) from common import ( PoetryModel, load_prompt_template, fill_prompt_template, filter_output, create_fill_in_blank, select_words_to_blank, load_themes, load_interests, check_input_safety ) def create_app(model: PoetryModel): """Create Poem Generator Gradio app with intelligent word selection and position-aware checking.""" prompt_path = os.path.join(os.path.dirname(__file__), 'prompts.md') prompt_template = load_prompt_template(prompt_path) themes_path = os.path.join(os.path.dirname(__file__), 'themes.md') interests_path = os.path.join(os.path.dirname(__file__), 'interests.md') THEMES = load_themes(themes_path) INTERESTS = load_interests(interests_path) MAX_INTERESTS = 3 def generate_poem(age, length, difficulty, selected_theme, selected_interests): # Convert age to int if it's a string try: age = int(age) except (ValueError, TypeError): age = 9 # Default # Build theme and interests strings theme_text = selected_theme if selected_theme else "general" interests_text = ", ".join(selected_interests) if selected_interests else "various things" prompt = fill_prompt_template( prompt_template, age=age, length=length, theme=theme_text, interests=interests_text ) # Generate poem with debugging output print(f"\n{'='*70}\nšŸŽØ GENERATING POEM\n{'='*70}") print(f"Age: {age} | Length: {length} | Difficulty: {difficulty}") print(f"Themes: {theme_text}") print(f"Interests: {interests_text}") print(f"\nPrompt sent to model:\n{'-'*70}\n{prompt}\n{'-'*70}") poem = model.generate(prompt, max_tokens=350, temperature=0.7) poem = filter_output(poem) print(f"\nāœ“ Generated Poem:\n{'-'*70}\n{poem}\n{'-'*70}") # Use AI to intelligently select words to blank selected_words = select_words_to_blank(poem, difficulty, age, model) # Create blanks with exact positions blanked, correct_answers, positions, all_words = create_fill_in_blank(poem, difficulty, selected_words) print(f"\nšŸ“Š Blank Information:") print(f"Number of blanks: {len(positions)}") print(f"Positions: {positions}") print(f"Correct answers: {correct_answers}") print(f"\nBlanked poem:\n{'-'*70}\n{blanked}\n{'='*70}\n") # Format poem with line breaks for better display poem_lines = blanked.split('. ') # Use double line break for clear separation formatted_blanked = "

".join([line.strip() + ('.' if i < len(poem_lines)-1 else '') for i, line in enumerate(poem_lines) if line.strip()]) # Create shuffled word bank import random word_bank = correct_answers.copy() random.shuffle(word_bank) word_bank_html = "**Word Bank:** " + " | ".join([f"{word}" for word in word_bank]) # Create input fields for each blank blank_components = [] for i in range(len(correct_answers)): blank_components.append(gr.update( value="", visible=True, placeholder=f"Blank {i+1}", interactive=True, type = 'text' )) # Hide unused textboxes for i in range(len(correct_answers), 9): blank_components.append(gr.update(visible=False)) # Store state with positions and correct answers state = { "correct_answers": correct_answers, "positions": positions, "all_words": all_words, "original_poem": poem, "word_bank": word_bank } return { output_markdown: gr.update(value=f"**Your Poem:**\n\n{formatted_blanked}\n\n{word_bank_html}", visible=True), blank_inputs: gr.update(visible=True), **{blank_textboxes[i]: blank_components[i] for i in range(9)}, check_button: gr.update(visible=True), solution_button: gr.update(visible=False), result_markdown: gr.update(visible=False), solution_display: gr.update(visible=False), poem_state: state } def check_answers(*args): """Check user answers with position-aware validation and color coding.""" state = args[-1] user_inputs = list(args[:-1]) correct_answers = state.get("correct_answers", []) positions = state.get("positions", []) all_words = state.get("all_words", []) word_bank = state.get("word_bank", []) # Recreate word bank HTML word_bank_html = "**Word Bank:** " + " | ".join([f"{word}" for word in word_bank]) # Check each answer against correct answer at same position results = [] for i, (user_word, correct_word) in enumerate(zip(user_inputs[:len(correct_answers)], correct_answers)): is_correct = (user_word or "").lower().strip() == correct_word.lower().strip() results.append(is_correct) # Build poem with user's answers color-coded (in place) display_words = all_words.copy() for i, pos in enumerate(positions): if i < len(user_inputs) and user_inputs[i]: # Color code based on correctness if results[i]: display_words[pos] = f"{user_inputs[i]}" else: display_words[pos] = f"{user_inputs[i]}" else: # Empty input - keep underscores in poem and mark as incorrect (red) display_words[pos] = f"_______________" # Format poem with line breaks (split at periods or newlines) poem_text = " ".join(display_words) # Try to detect natural line breaks in original poem poem_lines = poem_text.replace('\n', '. ').split('. ') formatted_poem = "

".join([line.strip() + ('.' if i < len(poem_lines)-1 and not line.endswith('.') else '') for i, line in enumerate(poem_lines) if line.strip()]) # Generate result message if all(results): message = "## šŸŽ‰ Perfect! All answers are correct!" show_solution = False else: incorrect_count = sum(1 for r in results if not r) empty_count = sum(1 for i, inp in enumerate(user_inputs[:len(correct_answers)]) if not inp) if empty_count > 0: message = f"## āš ļø {empty_count} blank(s) still empty, {incorrect_count} incorrect. Keep trying!" else: message = f"## āŒ {incorrect_count} incorrect. Try again or check the solution!" show_solution = True # Update the main output_markdown instead of result_markdown to avoid duplication return { output_markdown: gr.update(value=f"{message}\n\n**Your Poem:**\n\n{formatted_poem}\n\n{word_bank_html}", visible=True), result_markdown: gr.update(visible=False), # Hide result markdown as we use main output solution_button: gr.update(visible=show_solution), **{blank_textboxes[i]: gr.update() for i in range(9)} } def show_solution(state): """Show the complete poem with all correct answers in green.""" correct_answers = state.get("correct_answers", []) positions = state.get("positions", []) all_words = state.get("all_words", []) # Build poem with all correct answers in green solution_words = all_words.copy() for i, pos in enumerate(positions): solution_words[pos] = f"{correct_answers[i]}" # Format poem with line breaks (split at periods or newlines) poem_text = " ".join(solution_words) poem_lines = poem_text.replace('\n', '. ').split('. ') formatted_poem = "

".join([line.strip() + ('.' if i < len(poem_lines)-1 and not line.endswith('.') else '') for i, line in enumerate(poem_lines) if line.strip()]) # Update main output_markdown to show solution in place return { output_markdown: gr.update(value=f"## āœ… Complete Poem (Solution)\n\n{formatted_poem}", visible=True), solution_display: gr.update(visible=False), # Hide separate solution display solution_button: gr.update(visible=False), result_markdown: gr.update(visible=False) } with gr.Blocks(title="Poem Generator") as demo: gr.Markdown("# šŸ“ Poem Generator with Fill-in-the-Blank Quiz") gr.Markdown("Create a personalized poem and test your vocabulary!") with gr.Row(): age_input = gr.Textbox(value="9", label="Age", placeholder="Enter age (6-99)") length_dropdown = gr.Dropdown(["short", "medium", "long"], value="short", label="Length") difficulty_dropdown = gr.Dropdown(["Easy", "Medium", "Hard"], value="Easy", label="Difficulty") with gr.Row(): theme_dropdown = gr.Dropdown( choices=THEMES, label="Theme (select one)", value=THEMES[0] if THEMES else None ) with gr.Row(): interests_checkboxes = gr.CheckboxGroup( choices=INTERESTS, label=f"Interests (select up to {MAX_INTERESTS})", value=[] ) generate_button = gr.Button("✨ Generate Poem", variant="primary") output_markdown = gr.Markdown("", visible=False) # Container for blank input fields with gr.Column(visible=False) as blank_inputs: gr.Markdown("### Fill in the blanks") with gr.Row(): blank_textboxes = [ gr.Textbox(label="", visible=False, placeholder=f"Blank {i+1}", value="_______________", scale=1, show_label=False, type="text") for i in range(9) ] with gr.Row(): check_button = gr.Button("Check Answers", visible=False, variant="primary") solution_button = gr.Button("Show Solution", visible=False, variant="secondary") result_markdown = gr.Markdown("", visible=False) solution_display = gr.Markdown("", visible=False) poem_state = gr.State({}) # Limit interests selection def limit_interests(selected): if len(selected) > MAX_INTERESTS: return selected[:MAX_INTERESTS] return selected interests_checkboxes.change( limit_interests, inputs=[interests_checkboxes], outputs=[interests_checkboxes] ) generate_button.click( generate_poem, inputs=[age_input, length_dropdown, difficulty_dropdown, theme_dropdown, interests_checkboxes], outputs=[output_markdown, blank_inputs] + blank_textboxes + [check_button, solution_button, result_markdown, solution_display, poem_state], ) check_button.click( check_answers, inputs=blank_textboxes + [poem_state], outputs=[output_markdown, result_markdown, solution_button] + blank_textboxes ) solution_button.click( show_solution, inputs=[poem_state], outputs=[output_markdown, solution_display, solution_button, result_markdown] ) # Enable the Check Answers button only when all required blanks are filled def validate_blanks(*args): # args: first 9 are textbox values, last is state *vals, state = args n_required = len(state.get("correct_answers", [])) if isinstance(state, dict) else 0 # consider only first n_required textboxes all_filled = n_required > 0 and all((vals[i] or "").strip() for i in range(n_required)) return {check_button: gr.update(interactive=all_filled)} # Attach change handlers to all blank textboxes for tb in blank_textboxes: tb.change( validate_blanks, inputs=blank_textboxes + [poem_state], outputs=[check_button] ) return demo if __name__ == "__main__": from dotenv import load_dotenv load_dotenv() model = PoetryModel() app = create_app(model) app.launch()