Spaces:
Running
Running
| title: Pomodoro Timer | |
| emoji: ⏲️🌳 | |
| colorFrom: red | |
| colorTo: red | |
| sdk: gradio | |
| sdk_version: 6.5.1 | |
| app_file: pomodoro_forest.py | |
| pinned: false | |
| license: mit | |
| short_description: 'Gamified Pomodoro Timer: a pixel-art tree grows as you focus' | |
| # 🍅 PomodoroTimer Component Documentation | |
| A gamified Pomodoro timer built with Gradio 6's `gr.HTML` component. Watch pixel-art trees grow as you stay focused, and build a forest over time! | |
| --- | |
| ## Table of Contents | |
| - [Installation](#installation) | |
| - [Quick Start](#quick-start) | |
| - [Parameters](#parameters) | |
| - [Value Schema](#value-schema) | |
| - [Events](#events) | |
| - [Tree Themes](#tree-themes) | |
| - [Examples](#examples) | |
| - [Basic Usage](#basic-usage) | |
| - [Custom Durations](#custom-durations) | |
| - [Listening to Events](#listening-to-events) | |
| - [Updating the Timer Programmatically](#updating-the-timer-programmatically) | |
| - [API Usage](#api-usage) | |
| - [MCP (Model Context Protocol) Usage](#mcp-usage) | |
| - [Customization](#customization) | |
| - [Best Practices](#best-practices) | |
| --- | |
| ## Installation | |
| The component is a single Python file. Copy `pomodoro_forest.py` into your project or import the `PomodoroTimer` class directly. | |
| **Requirements:** | |
| - Gradio 6.0+ | |
| - Python 3.9+ | |
| ```bash | |
| pip install "gradio>=6.0" | |
| ``` | |
| --- | |
| ## Quick Start | |
| ```python | |
| import gradio as gr | |
| from pomodoro_forest import PomodoroTimer | |
| with gr.Blocks() as demo: | |
| timer = PomodoroTimer() | |
| demo.launch() | |
| ``` | |
| --- | |
| ## Parameters | |
| | Parameter | Type | Default | Description | | |
| |-----------|------|---------|-------------| | |
| | `value` | `dict` | See below | Timer state (elapsed time, sessions, etc.) | | |
| | `duration` | `int` | `25` | Duration of the current mode in minutes | | |
| | `mode` | `str` | `"focus"` | Current mode: `"focus"`, `"short_break"`, or `"long_break"` | | |
| | `tree_theme` | `str` | `"classic"` | Visual theme for the tree (see [Tree Themes](#tree-themes)) | | |
| | `mode_color` | `str` | Auto | Override the mode's accent color (hex) | | |
| | `trunk_color` | `str` | Auto | Override trunk color (hex) | | |
| | `crown_color` | `str` | Auto | Override crown/leaves color (hex) | | |
| | `crown_top_color` | `str` | Auto | Override crown top color (hex) | | |
| | `fruit_color` | `str` | Auto | Override fruit color (hex) | | |
| --- | |
| ## Value Schema | |
| The `value` parameter is a dictionary with the following structure: | |
| ```python | |
| { | |
| "elapsed": int, # Seconds elapsed in current session (0 to duration*60) | |
| "running": bool, # Whether the timer is currently running | |
| "sessions": int, # Number of completed focus sessions (trees grown) | |
| "total_minutes": int # Total minutes spent in focus mode | |
| } | |
| ``` | |
| **Default value:** | |
| ```python | |
| {"elapsed": 0, "running": False, "sessions": 0, "total_minutes": 0} | |
| ``` | |
| **API Schema (JSON):** | |
| ```json | |
| { | |
| "type": "object", | |
| "properties": { | |
| "elapsed": {"type": "integer"}, | |
| "running": {"type": "boolean"}, | |
| "sessions": {"type": "integer"}, | |
| "total_minutes": {"type": "integer"} | |
| } | |
| } | |
| ``` | |
| --- | |
| ## Events | |
| The PomodoroTimer component emits the following events: | |
| | Event | Trigger | Event Data | Description | | |
| |-------|---------|------------|-------------| | |
| | `.submit()` | Session completes | None | Fired when a focus/break session reaches 100% | | |
| | `.select()` | Mode button clicked | `{"mode": str}` | Fired when user clicks Focus/Short Break/Long Break | | |
| | `.change()` | Value changes | None | Fired on any value change (every second while running) | | |
| ### Accessing Event Data | |
| ```python | |
| def handle_mode_select(evt: gr.EventData, timer_val): | |
| # Safely access the mode from event data | |
| new_mode = evt._data.get("mode", "focus") if evt._data else "focus" | |
| return new_mode | |
| timer.select(fn=handle_mode_select, inputs=[timer], outputs=[...]) | |
| ``` | |
| --- | |
| ## Tree Themes | |
| Five built-in themes are available: | |
| | Theme | Trunk | Crown | Fruit | Best For | | |
| |-------|-------|-------|-------|----------| | |
| | `classic` | Brown | Green | Red | Default look | | |
| | `cherry` | Dark brown | Pink | Hot pink | Spring vibes | | |
| | `autumn` | Brown | Orange | Deep orange | Fall season | | |
| | `winter` | Gray | Silver | Light blue | Winter/holidays | | |
| | `sakura` | Dark brown | Light pink | Pink | Japanese aesthetic | | |
| **Theme colors (for reference):** | |
| ```python | |
| TREE_THEMES = { | |
| "classic": {"trunk": "#8B5E3C", "crown": "#2ecc71", "crown_top": "#00b894", "fruit": "#e74c3c"}, | |
| "cherry": {"trunk": "#5D4037", "crown": "#F8BBD9", "crown_top": "#F48FB1", "fruit": "#E91E63"}, | |
| "autumn": {"trunk": "#6D4C41", "crown": "#FF9800", "crown_top": "#FFC107", "fruit": "#FF5722"}, | |
| "winter": {"trunk": "#455A64", "crown": "#B0BEC5", "crown_top": "#ECEFF1", "fruit": "#81D4FA"}, | |
| "sakura": {"trunk": "#4E342E", "crown": "#FCE4EC", "crown_top": "#F8BBD0", "fruit": "#EC407A"}, | |
| } | |
| ``` | |
| --- | |
| ## Examples | |
| ### Basic Usage | |
| ```python | |
| import gradio as gr | |
| from pomodoro_forest import PomodoroTimer | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# My Pomodoro App") | |
| timer = PomodoroTimer(duration=25, mode="focus", tree_theme="classic") | |
| demo.launch() | |
| ``` | |
| ### Custom Durations | |
| ```python | |
| import gradio as gr | |
| from pomodoro_forest import PomodoroTimer, _update_timer | |
| with gr.Blocks() as demo: | |
| timer = PomodoroTimer(duration=50, mode="focus") # 50-minute focus | |
| # Quick preset buttons | |
| with gr.Row(): | |
| btn_25 = gr.Button("25 min") | |
| btn_50 = gr.Button("50 min") | |
| btn_25.click( | |
| fn=lambda v: _update_timer({**v, "elapsed": 0}, 25, "focus", "classic"), | |
| inputs=[timer], | |
| outputs=[timer] | |
| ) | |
| btn_50.click( | |
| fn=lambda v: _update_timer({**v, "elapsed": 0}, 50, "focus", "classic"), | |
| inputs=[timer], | |
| outputs=[timer] | |
| ) | |
| demo.launch() | |
| ``` | |
| ### Listening to Events | |
| ```python | |
| import gradio as gr | |
| from pomodoro_forest import PomodoroTimer, _update_timer | |
| with gr.Blocks() as demo: | |
| timer = PomodoroTimer() | |
| status = gr.Textbox(label="Status") | |
| # When a session completes | |
| def on_complete(timer_val): | |
| sessions = timer_val.get("sessions", 0) | |
| return f"🎉 Congratulations! You've grown {sessions} trees!" | |
| timer.submit(fn=on_complete, inputs=[timer], outputs=[status]) | |
| demo.launch() | |
| ``` | |
| ### Updating the Timer Programmatically | |
| > ⚠️ **Important:** Always use `gr.HTML(...)` to update props, never return a new `PomodoroTimer()` instance. | |
| ```python | |
| import gradio as gr | |
| from pomodoro_forest import PomodoroTimer, _update_timer, TREE_THEMES, MODE_COLORS | |
| with gr.Blocks() as demo: | |
| timer = PomodoroTimer() | |
| # Add 5 sessions programmatically (for testing) | |
| def add_sessions(timer_val): | |
| new_val = { | |
| **timer_val, | |
| "sessions": timer_val.get("sessions", 0) + 5, | |
| "total_minutes": timer_val.get("total_minutes", 0) + 125 | |
| } | |
| return _update_timer(new_val, 25, "focus", "classic") | |
| btn = gr.Button("Add 5 Sessions (Demo)") | |
| btn.click(fn=add_sessions, inputs=[timer], outputs=[timer]) | |
| demo.launch() | |
| ``` | |
| --- | |
| ## API Usage | |
| When your Gradio app is running, you can interact with the PomodoroTimer via the API. | |
| ### Get Current State | |
| ```python | |
| from gradio_client import Client | |
| client = Client("http://localhost:7860") | |
| # If your timer is an input to an API endpoint | |
| result = client.predict( | |
| {"elapsed": 0, "running": False, "sessions": 3, "total_minutes": 75}, | |
| api_name="/your_endpoint" | |
| ) | |
| ``` | |
| ### Python Client Example | |
| ```python | |
| from gradio_client import Client | |
| client = Client("http://localhost:7860") | |
| # Start a session with pre-existing data | |
| timer_state = { | |
| "elapsed": 0, | |
| "running": False, | |
| "sessions": 5, | |
| "total_minutes": 125 | |
| } | |
| # Call your function that takes timer as input | |
| result = client.predict(timer_state, api_name="/process_timer") | |
| print(result) | |
| ``` | |
| ### REST API | |
| ```bash | |
| curl -X POST http://localhost:7860/api/your_endpoint \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "data": [{ | |
| "elapsed": 0, | |
| "running": false, | |
| "sessions": 10, | |
| "total_minutes": 250 | |
| }] | |
| }' | |
| ``` | |
| --- | |
| ## MCP Usage | |
| The PomodoroTimer component works with Gradio's MCP (Model Context Protocol) support, allowing AI assistants to interact with it. | |
| ### Exposing via MCP | |
| ```python | |
| import gradio as gr | |
| from pomodoro_forest import PomodoroTimer, _update_timer | |
| with gr.Blocks() as demo: | |
| timer = PomodoroTimer() | |
| output = gr.JSON(label="Timer State") | |
| def get_timer_state(timer_val): | |
| """Get the current Pomodoro timer state. | |
| Returns the timer's current state including elapsed time, | |
| running status, completed sessions, and total focus minutes. | |
| """ | |
| return timer_val | |
| def set_timer_sessions(timer_val, sessions: int, total_minutes: int): | |
| """Set the Pomodoro timer's session count. | |
| Args: | |
| sessions: Number of completed focus sessions | |
| total_minutes: Total minutes spent focusing | |
| """ | |
| new_val = {**timer_val, "sessions": sessions, "total_minutes": total_minutes} | |
| return _update_timer(new_val, 25, "focus", "classic"), new_val | |
| # Expose as API endpoints for MCP | |
| get_btn = gr.Button("Get State") | |
| get_btn.click( | |
| fn=get_timer_state, | |
| inputs=[timer], | |
| outputs=[output], | |
| api_name="get_pomodoro_state" # MCP-accessible endpoint | |
| ) | |
| with gr.Row(): | |
| sessions_input = gr.Number(label="Sessions", value=0) | |
| minutes_input = gr.Number(label="Total Minutes", value=0) | |
| set_btn = gr.Button("Set Sessions") | |
| set_btn.click( | |
| fn=set_timer_sessions, | |
| inputs=[timer, sessions_input, minutes_input], | |
| outputs=[timer, output], | |
| api_name="set_pomodoro_sessions" # MCP-accessible endpoint | |
| ) | |
| demo.launch() | |
| ``` | |
| ### MCP Tool Definitions | |
| When used with MCP, the following tools become available: | |
| **`get_pomodoro_state`** | |
| ```json | |
| { | |
| "name": "get_pomodoro_state", | |
| "description": "Get the current Pomodoro timer state", | |
| "parameters": { | |
| "timer_val": { | |
| "type": "object", | |
| "properties": { | |
| "elapsed": {"type": "integer"}, | |
| "running": {"type": "boolean"}, | |
| "sessions": {"type": "integer"}, | |
| "total_minutes": {"type": "integer"} | |
| } | |
| } | |
| } | |
| } | |
| ``` | |
| **`set_pomodoro_sessions`** | |
| ```json | |
| { | |
| "name": "set_pomodoro_sessions", | |
| "description": "Set the Pomodoro timer's session count", | |
| "parameters": { | |
| "sessions": {"type": "integer", "description": "Number of completed sessions"}, | |
| "total_minutes": {"type": "integer", "description": "Total focus minutes"} | |
| } | |
| } | |
| ``` | |
| ### Using with Claude or Other MCP Clients | |
| ```python | |
| # Example: AI assistant querying your Pomodoro app via MCP | |
| # The assistant can call these tools to interact with the timer | |
| # Get current state | |
| state = mcp_client.call_tool("get_pomodoro_state", {}) | |
| # Returns: {"elapsed": 300, "running": true, "sessions": 3, "total_minutes": 75} | |
| # Set sessions (e.g., restore from saved data) | |
| mcp_client.call_tool("set_pomodoro_sessions", { | |
| "sessions": 10, | |
| "total_minutes": 250 | |
| }) | |
| ``` | |
| --- | |
| ## Customization | |
| ### Adding Custom Themes | |
| ```python | |
| from pomodoro_forest import TREE_THEMES | |
| # Add your own theme | |
| TREE_THEMES["ocean"] = { | |
| "trunk": "#1565C0", | |
| "crown": "#4FC3F7", | |
| "crown_top": "#81D4FA", | |
| "fruit": "#00BCD4" | |
| } | |
| # Use it | |
| timer = PomodoroTimer(tree_theme="ocean") | |
| ``` | |
| ### Override Individual Colors | |
| ```python | |
| timer = PomodoroTimer( | |
| tree_theme="classic", | |
| crown_color="#9C27B0", # Purple crown | |
| fruit_color="#FFEB3B", # Yellow fruit | |
| ) | |
| ``` | |
| ### Custom Mode Colors | |
| ```python | |
| from pomodoro_forest import MODE_COLORS | |
| MODE_COLORS["focus"] = "#9C27B0" # Purple for focus | |
| MODE_COLORS["short_break"] = "#00BCD4" # Cyan for short break | |
| MODE_COLORS["long_break"] = "#FF9800" # Orange for long break | |
| ``` | |
| --- | |
| ## Best Practices | |
| ### 1. Always Use `gr.HTML()` for Updates | |
| ```python | |
| # ✅ Correct | |
| def update_timer(timer_val): | |
| return gr.HTML(value=new_val, duration=25, mode="focus", ...) | |
| # ❌ Wrong - causes issues | |
| def update_timer(timer_val): | |
| return PomodoroTimer(value=new_val, duration=25, mode="focus") | |
| ``` | |
| ### 2. Use Helper Functions | |
| Import and use the provided helper functions: | |
| ```python | |
| from pomodoro_forest import _update_timer, _update_theme_only | |
| # Full update | |
| timer_output = _update_timer(value, duration, mode, theme) | |
| # Theme-only update | |
| timer_output = _update_theme_only(theme, mode) | |
| ``` | |
| ### 3. Handle Events Safely | |
| ```python | |
| def handle_event(evt: gr.EventData, timer_val): | |
| # Always check if _data exists | |
| try: | |
| data = evt._data.get("key", "default") if evt._data else "default" | |
| except: | |
| data = "default" | |
| return data | |
| ``` | |
| ### 4. Preserve State Across Updates | |
| ```python | |
| def my_handler(timer_val, new_duration): | |
| # Spread existing state, only change what's needed | |
| new_val = {**timer_val, "elapsed": 0} | |
| return _update_timer(new_val, new_duration, "focus", "classic") | |
| ``` | |
| --- | |
| ## Component Architecture | |
| ``` | |
| PomodoroTimer (extends gr.HTML) | |
| ├── html_template → Renders timer ring, tree scene, controls, stats | |
| ├── css_template → Styles with ${prop} placeholders for dynamic colors | |
| ├── js_on_load → Handles start/pause, reset, mode switching | |
| └── value → Dict holding timer state | |
| ``` | |
| **File Structure:** | |
| ``` | |
| pomodoro_forest.py | |
| ├── Constants (DURATIONS, MODE_COLORS, TREE_THEMES) | |
| ├── Templates (HTML_TEMPLATE, CSS_TEMPLATE, JS_ON_LOAD) | |
| ├── PomodoroTimer class | |
| ├── Helper functions (_update_timer, _update_theme_only) | |
| └── Demo app (with gr.Blocks) | |
| ``` | |
| --- | |
| ## Troubleshooting | |
| | Issue | Cause | Solution | | |
| |-------|-------|----------| | |
| | Colors don't update | Using Python string formatting instead of `${prop}` | Use template syntax: `${mode_color}` | | |
| | "Multiple values for argument" error | Returning subclass instance | Return `gr.HTML(...)` instead | | |
| | Event data is None | Accessing wrong event or wrong data key | Check `evt._data` exists before accessing | | |
| | Timer doesn't re-render | Mutating value in place | Always spread: `{...timer_val, key: newVal}` | | |
| --- | |
| ## License | |
| MIT License - Feel free to use, modify, and distribute! | |
| --- | |
| ## Credits | |
| Built with [Gradio 6](https://gradio.app) and the new `gr.HTML` custom component system. | |
| Created as a demo for the Gradio HTML component capabilities. |