""" Creative Program Plan Editor - Gradio 6 Web Application A web-based program plan editor that allows users to: - Create and arrange content elements on a canvas - Lock template elements to prevent editing - Add text and image placeholders - Load Word documents and export to PDF - Grid-based positioning with snap-to-grid Built with anycoder: https://huggingface.co/spaces/akhaliq/anycoder """ import gradio as gr import json from typing import List, Dict, Any from datetime import datetime # --- Model: Data Structure --- class CanvasElement: """Base class for canvas elements""" def __init__(self, x: int, y: int, width: int, height: int, element_type: str = "text"): self.x = x self.y = y self.width = width self.height = height self.element_type = element_type self.is_selected = False self.is_locked = False def to_dict(self) -> Dict[str, Any]: return { "x": self.x, "y": self.y, "width": self.width, "height": self.height, "type": self.element_type, "locked": self.is_locked, "selected": self.is_selected } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "CanvasElement": element = cls( data["x"], data["y"], data["width"], data["height"], data["type"] ) element.is_locked = data.get("locked", False) element.is_selected = data.get("selected", False) return element class TextElement(CanvasElement): """Text element for the canvas""" def __init__(self, text: str, x: int, y: int, width: int, height: int): super().__init__(x, y, width, height, "text") self.text = text def to_dict(self) -> Dict[str, Any]: data = super().to_dict() data["text"] = self.text return data @classmethod def from_dict(cls, data: Dict[str, Any]) -> "TextElement": element = cls( data["text"], data["x"], data["y"], data["width"], data["height"] ) element.is_locked = data.get("locked", False) element.is_selected = data.get("selected", False) return element class ImageElement(CanvasElement): """Image placeholder element for the canvas""" def __init__(self, image_path: str, x: int, y: int, width: int, height: int): super().__init__(x, y, width, height, "image") self.image_path = image_path def to_dict(self) -> Dict[str, Any]: data = super().to_dict() data["image_path"] = self.image_path return data @classmethod def from_dict(cls, data: Dict[str, Any]) -> "ImageElement": element = cls( data["image_path"], data["x"], data["y"], data["width"], data["height"] ) element.is_locked = data.get("locked", False) element.is_selected = data.get("selected", False) return element # --- Canvas State Management --- def create_canvas_state() -> Dict[str, Any]: """Create initial canvas state""" return { "elements": [], "template_mode": False, "grid_size": 50, "selected_element_index": -1 } def serialize_elements(elements: List[CanvasElement]) -> str: """Serialize elements to JSON string""" return json.dumps([el.to_dict() for el in elements]) def deserialize_elements(elements_json: str) -> List[CanvasElement]: """Deserialize elements from JSON string""" if not elements_json: return [] elements = [] data = json.loads(elements_json) for item in data: if item["type"] == "text": elements.append(TextElement.from_dict(item)) elif item["type"] == "image": elements.append(ImageElement.from_dict(item)) else: elements.append(CanvasElement.from_dict(item)) return elements # --- Canvas HTML Generator --- def generate_canvas_html(elements: List[CanvasElement], grid_size: int = 50, canvas_width: int = 800, canvas_height: int = 600) -> str: """Generate HTML for the interactive canvas""" # Create grid background grid_lines = [] for x in range(0, canvas_width + 1, grid_size): grid_lines.append(f'') for y in range(0, canvas_height + 1, grid_size): grid_lines.append(f'') # Create element HTML elements_html = [] for i, el in enumerate(elements): bg_color = "#B0C4DE" if el.element_type == "image" else "#ffffff" border_color = "#0078d7" if el.is_selected else "#cccccc" border_width = "2" if el.is_selected else "1" lock_icon = "🔒" if el.is_locked else "" if el.element_type == "text": content = el.text if hasattr(el, 'text') else "Text" element_content = f'
{content}
' else: element_content = '
📷 Image
' elements_html.append(f'''
{element_content}
{lock_icon}
''') html = f'''
{''.join(grid_lines)} {''.join(elements_html)}
''' return html # --- Main Application Functions --- def initialize_canvas() -> tuple: """Initialize the canvas with sample template""" elements = [] # Create header row header_y = 50 header_height = 40 headers = ["Learning Areas", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] header_x = [100, 200, 300, 400, 500, 600] for i, header_text in enumerate(headers): el = TextElement(header_text, header_x[i], header_y, 80, header_height) el.is_locked = True elements.append(el) # Row 2: Art Experiences row2_y = header_y + header_height + 20 art_label = TextElement("Art Experiences", 100, row2_y, 100, 40) art_label.is_locked = True elements.append(art_label) elements.append(TextElement("Painting", 200, row2_y, 80, 40)) elements.append(ImageElement("art.png", 300, row2_y, 80, 40)) # Row 3: Sensory Experiences row3_y = row2_y + 80 sensory_label = TextElement("Sensory", 100, row3_y, 100, 40) sensory_label.is_locked = True elements.append(sensory_label) for i in range(4): elements.append(TextElement("Sand", 200 + (i * 100), row3_y, 80, 40)) canvas_html = generate_canvas_html(elements) elements_json = serialize_elements(elements) return canvas_html, elements_json, len(elements), "Template loaded with 15 elements" def add_text_element(elements_json: str, text: str, x: int, y: int, width: int, height: int) -> tuple: """Add a text element to the canvas""" elements = deserialize_elements(elements_json) new_element = TextElement(text, x, y, width, height) elements.append(new_element) canvas_html = generate_canvas_html(elements) elements_json = serialize_elements(elements) return canvas_html, elements_json, len(elements), f"Added text element: '{text}'" def add_image_element(elements_json: str, x: int, y: int, width: int, height: int) -> tuple: """Add an image placeholder to the canvas""" elements = deserialize_elements(elements_json) new_element = ImageElement("placeholder.png", x, y, width, height) elements.append(new_element) canvas_html = generate_canvas_html(elements) elements_json = serialize_elements(elements) return canvas_html, elements_json, len(elements), "Added image placeholder" def toggle_template_mode(elements_json: str, template_mode: bool) -> tuple: """Toggle template mode (lock/unlock all elements)""" elements = deserialize_elements(elements_json) for el in elements: el.is_locked = template_mode canvas_html = generate_canvas_html(elements) elements_json = serialize_elements(elements) mode_status = "enabled" if template_mode else "disabled" return canvas_html, elements_json, f"Template mode {mode_status}" def clear_canvas() -> tuple: """Clear all elements from canvas""" canvas_html = generate_canvas_html([]) return canvas_html, "", 0, "Canvas cleared" def load_word_document(file) -> tuple: """Load elements from a Word document (simulated)""" if file is None: return generate_canvas_html([]), "", 0, "No file selected" # Simulate loading from Word document elements = [] # Create a sample layout based on document structure elements.append(TextElement("Document Title", 100, 50, 200, 40)) elements.append(TextElement("Section 1", 100, 120, 150, 30)) elements.append(TextElement("Content from DOCX", 100, 170, 300, 30)) elements.append(ImageElement("doc_image.png", 100, 220, 150, 100)) canvas_html = generate_canvas_html(elements) elements_json = serialize_elements(elements) return canvas_html, elements_json, len(elements), f"Loaded from: {file.name}" def export_to_pdf(elements_json: str) -> str: """Export canvas to PDF (simulated)""" elements = deserialize_elements(elements_json) if not elements: return "No elements to export" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"program_plan_{timestamp}.pdf" return f"PDF exported successfully: {filename} ({len(elements)} elements)" def update_element_position(elements_json: str, element_index: int, new_x: int, new_y: int, grid_size: int = 50) -> tuple: """Update element position with grid snapping""" elements = deserialize_elements(elements_json) if 0 <= element_index < len(elements): # Snap to grid snap_x = round(new_x / grid_size) * grid_size snap_y = round(new_y / grid_size) * grid_size elements[element_index].x = snap_x elements[element_index].y = snap_y canvas_html = generate_canvas_html(elements) elements_json = serialize_elements(elements) return canvas_html, elements_json, f"Moved element to ({snap_x}, {snap_y})" return generate_canvas_html(elements), elements_json, "Invalid element index" def select_element(elements_json: str, element_index: int) -> tuple: """Select an element on the canvas""" elements = deserialize_elements(elements_json) # Deselect all for el in elements: el.is_selected = False # Select specified element if 0 <= element_index < len(elements): elements[element_index].is_selected = True element_info = f"Selected: {elements[element_index].element_type} at ({elements[element_index].x}, {elements[element_index].y})" else: element_info = "No element selected" canvas_html = generate_canvas_html(elements) elements_json = serialize_elements(elements) return canvas_html, elements_json, element_info def delete_selected_element(elements_json: str) -> tuple: """Delete the currently selected element""" elements = deserialize_elements(elements_json) # Find and remove selected element original_count = len(elements) elements = [el for el in elements if not el.is_selected] if len(elements) < original_count: message = "Deleted selected element" else: message = "No element selected to delete" canvas_html = generate_canvas_html(elements) elements_json = serialize_elements(elements) return canvas_html, elements_json, len(elements), message def update_element_text(elements_json: str, element_index: int, new_text: str) -> tuple: """Update text of a selected element""" elements = deserialize_elements(elements_json) if 0 <= element_index < len(elements): if elements[element_index].element_type == "text": elements[element_index].text = new_text message = f"Updated text to: '{new_text}'" else: message = "Selected element is not a text element" else: message = "Invalid element index" canvas_html = generate_canvas_html(elements) elements_json = serialize_elements(elements) return canvas_html, elements_json, message def add_content_library_item(elements_json: str, content_type: str) -> tuple: """Add predefined content from library""" elements = deserialize_elements(elements_json) # Find a good position for new element base_x = 100 base_y = 400 + (len(elements) * 50) if content_type == "sand": elements.append(TextElement("Sand Experience", base_x, base_y, 150, 40)) message = "Added Sand Experience" elif content_type == "art": elements.append(TextElement("Art Activity", base_x, base_y, 150, 40)) message = "Added Art Activity" elif content_type == "music": elements.append(TextElement("Music Time", base_x, base_y, 150, 40)) message = "Added Music Time" elif content_type == "image": elements.append(ImageElement("placeholder.png", base_x, base_y, 150, 100)) message = "Added Image Placeholder" else: message = "Unknown content type" canvas_html = generate_canvas_html(elements) elements_json = serialize_elements(elements) return canvas_html, elements_json, len(elements), message # --- Gradio 6 Application --- with gr.Blocks() as demo: gr.Markdown(""" # 🎨 Creative Program Plan Editor Design and arrange your program plans with this interactive canvas editor. Add text, images, and organize your content with grid-based positioning. **Built with anycoder**: [https://huggingface.co/spaces/akhaliq/anycoder](https://huggingface.co/spaces/akhaliq/anycoder) """) # State to store canvas elements elements_state = gr.State(value="") with gr.Row(): # Left Panel: Canvas with gr.Column(scale=3): canvas_output = gr.HTML( value=generate_canvas_html([]), label="Canvas", elem_classes=["canvas-container"] ) with gr.Row(): element_count = gr.Number(label="Elements", value=0, interactive=False) status_message = gr.Textbox(label="Status", interactive=False) # Right Panel: Controls with gr.Column(scale=1): gr.Markdown("### 🛠️ Tools") # Add Elements with gr.Accordion("Add Elements", open=True): text_input = gr.Textbox(label="Text Content", placeholder="Enter text...") with gr.Row(): pos_x = gr.Number(label="X Position", value=100, step=50) pos_y = gr.Number(label="Y Position", value=100, step=50) with gr.Row(): elem_width = gr.Number(label="Width", value=100, minimum=50) elem_height = gr.Number(label="Height", value=40, minimum=30) with gr.Row(): add_text_btn = gr.Button("➕ Add Text", variant="primary") add_image_btn = gr.Button("🖼️ Add Image") # Content Library with gr.Accordion("📚 Content Library", open=False): gr.Markdown("Quick add predefined content:") with gr.Row(): add_sand_btn = gr.Button("🏖️ Sand") add_art_btn = gr.Button("🎨 Art") with gr.Row(): add_music_btn = gr.Button("🎵 Music") add_img_lib_btn = gr.Button("📷 Image") # Document Operations with gr.Accordion("📄 Document", open=False): file_input = gr.File(label="Load Word Document", file_types=[".docx"]) load_doc_btn = gr.Button("📥 Load Document") export_pdf_btn = gr.Button("📤 Export to PDF") # Canvas Controls with gr.Accordion("⚙️ Canvas Controls", open=False): template_mode = gr.Checkbox(label="🔒 Lock Template Elements", value=False) grid_size = gr.Slider(10, 100, value=50, step=10, label="Grid Size") with gr.Row(): init_canvas_btn = gr.Button("🔄 Load Template") clear_canvas_btn = gr.Button("🗑️ Clear All") # Element Editing with gr.Accordion("✏️ Edit Selected", open=False): selected_index = gr.Number(label="Element Index", value=-1, precision=0) edit_text_input = gr.Textbox(label="Edit Text", placeholder="New text...") with gr.Row(): select_btn = gr.Button("🎯 Select") delete_btn = gr.Button("❌ Delete", variant="stop") update_text_btn = gr.Button("💾 Update Text") # Event Listeners # Initialize canvas init_canvas_btn.click( fn=initialize_canvas, inputs=[], outputs=[canvas_output, elements_state, element_count, status_message] ) # Add text element add_text_btn.click( fn=add_text_element, inputs=[elements_state, text_input, pos_x, pos_y, elem_width, elem_height], outputs=[canvas_output, elements_state, element_count, status_message] ).then( fn=lambda: "", inputs=[], outputs=[text_input] ) # Add image element add_image_btn.click( fn=add_image_element, inputs=[elements_state, pos_x, pos_y, elem_width, elem_height], outputs=[canvas_output, elements_state, element_count, status_message] ) # Toggle template mode template_mode.change( fn=toggle_template_mode, inputs=[elements_state, template_mode], outputs=[canvas_output, elements_state, status_message] ) # Clear canvas clear_canvas_btn.click( fn=clear_canvas, inputs=[], outputs=[canvas_output, elements_state, element_count, status_message] ) # Load Word document load_doc_btn.click( fn=load_word_document, inputs=[file_input], outputs=[canvas_output, elements_state, element_count, status_message] ) # Export to PDF export_pdf_btn.click( fn=export_to_pdf, inputs=[elements_state], outputs=[status_message] ) # Content library buttons add_sand_btn.click( fn=add_content_library_item, inputs=[elements_state, gr.State("sand")], outputs=[canvas_output, elements_state, element_count, status_message] ) add_art_btn.click( fn=add_content_library_item, inputs=[elements_state, gr.State("art")], outputs=[canvas_output, elements_state, element_count, status_message] ) add_music_btn.click( fn=add_content_library_item, inputs=[elements_state, gr.State("music")], outputs=[canvas_output, elements_state, element_count, status_message] ) add_img_lib_btn.click( fn=add_content_library_item, inputs=[elements_state, gr.State("image")], outputs=[canvas_output, elements_state, element_count, status_message] ) # Element editing select_btn.click( fn=select_element, inputs=[elements_state, selected_index], outputs=[canvas_output, elements_state, status_message] ) delete_btn.click( fn=delete_selected_element, inputs=[elements_state], outputs=[canvas_output, elements_state, element_count, status_message] ) update_text_btn.click( fn=update_element_text, inputs=[elements_state, selected_index, edit_text_input], outputs=[canvas_output, elements_state, status_message] ) # Custom CSS for better canvas appearance demo.load( fn=lambda: None, inputs=[], outputs=[] ) # Launch with Gradio 6 syntax - theme goes in launch(), NOT in Blocks! demo.launch( theme=gr.themes.Soft( primary_hue="blue", secondary_hue="indigo", neutral_hue="slate", text_size="lg", spacing_size="md" ), footer_links=[ {"label": "Built with anycoder", "url": "https://huggingface.co/spaces/akhaliq/anycoder"} ], css=""" .canvas-container { border: 2px solid #e0e0e0; border-radius: 8px; background: #fafafa; } .gradio-container { max-width: 1400px !important; } #canvas-container { box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .canvas-element:hover { box-shadow: 0 4px 12px rgba(0,120,215,0.3); } """ )