"""
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(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);
}
"""
)