Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| } | |
| 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 | |
| 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 | |
| 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'<line x1="{x}" y1="0" x2="{x}" y2="{canvas_height}" stroke="#e0e0e0" stroke-width="1"/>') | |
| for y in range(0, canvas_height + 1, grid_size): | |
| grid_lines.append(f'<line x1="0" y1="{y}" x2="{canvas_width}" y2="{y}" stroke="#e0e0e0" stroke-width="1"/>') | |
| # 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'<div style="padding: 5px; overflow: hidden;">{content}</div>' | |
| else: | |
| element_content = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666;">π· Image</div>' | |
| elements_html.append(f''' | |
| <div class="canvas-element" data-index="{i}" | |
| style="position: absolute; left: {el.x}px; top: {el.y}px; | |
| width: {el.width}px; height: {el.height}px; | |
| background: {bg_color}; border: {border_width}px solid {border_color}; | |
| border-radius: 4px; cursor: move; user-select: none;" | |
| onclick="selectElement({i})"> | |
| {element_content} | |
| <div style="position: absolute; top: 2px; right: 2px; font-size: 12px;">{lock_icon}</div> | |
| </div> | |
| ''') | |
| html = f''' | |
| <div id="canvas-container" style="position: relative; width: {canvas_width}px; height: {canvas_height}px; | |
| background: white; border: 1px solid #ddd; overflow: hidden;"> | |
| <svg width="{canvas_width}" height="{canvas_height}" style="position: absolute; top: 0; left: 0; pointer-events: none;"> | |
| {''.join(grid_lines)} | |
| </svg> | |
| {''.join(elements_html)} | |
| </div> | |
| <script> | |
| function selectElement(index) {{ | |
| // This will be handled by Gradio state updates | |
| console.log('Selected element:', index); | |
| </script> | |
| ''' | |
| 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); | |
| } | |
| """ | |
| ) |