Commit
Β·
2c888f0
1
Parent(s):
7725750
feat(ui): add durations per task and copyable mermaid diagram text
Browse files- Adds an editable table for durations per task in minutes, with state storage
- uses per-task durations if provided
- Adds output code blocks to let user easily copy/paste the raw mermaid.js code for Gantt and flowchart visualizations
- UI in main gradio interface updated, including sync events
π€ Generated with opencode
Co-Authored-By: opencode <noreply@opencode.ai>
- OpenCode.md +6 -5
- app.py +112 -23
- test_app.py +13 -1
OpenCode.md
CHANGED
@@ -1,10 +1,11 @@
|
|
1 |
# OpenCode Guide for or-tools repo
|
2 |
|
3 |
## Commands
|
4 |
-
- **Install deps:** `
|
5 |
-
- **
|
6 |
-
- **
|
7 |
-
- **
|
|
|
8 |
- **No tests currently found.**
|
9 |
|
10 |
## Code Style & Conventions
|
@@ -16,6 +17,6 @@
|
|
16 |
- **Error handling:** Prefer informative exceptions & user messages. Wrap untrusted input in try/except when user-facing.
|
17 |
- **Docstrings:** Use triple-double-quote for all public functions/classes. Prefer Google or NumPy style.
|
18 |
- **No tests yet:** Place tests in files named `test_*.py` when adding.
|
19 |
-
- **Dependencies:** Only add to `pyproject.toml
|
20 |
|
21 |
*See https://docs.astral.sh/ruff/ for further lint rules. Contribute new code in line with this guide.*
|
|
|
1 |
# OpenCode Guide for or-tools repo
|
2 |
|
3 |
## Commands
|
4 |
+
- **Install deps:** `uv sync`
|
5 |
+
- **Add deps: `uv add dep`
|
6 |
+
- **Lint:** `uv run ruff check app.py`
|
7 |
+
- **Format:** `uv run ruff format app.py`
|
8 |
+
- **Run Gradio app:** `uv run app.py`
|
9 |
- **No tests currently found.**
|
10 |
|
11 |
## Code Style & Conventions
|
|
|
17 |
- **Error handling:** Prefer informative exceptions & user messages. Wrap untrusted input in try/except when user-facing.
|
18 |
- **Docstrings:** Use triple-double-quote for all public functions/classes. Prefer Google or NumPy style.
|
19 |
- **No tests yet:** Place tests in files named `test_*.py` when adding.
|
20 |
+
- **Dependencies:** Only add to `pyproject.toml`
|
21 |
|
22 |
*See https://docs.astral.sh/ruff/ for further lint rules. Contribute new code in line with this guide.*
|
app.py
CHANGED
@@ -139,29 +139,39 @@ def parse_tasks(tasks_text):
|
|
139 |
return sorted(tasks), original_names
|
140 |
|
141 |
|
142 |
-
def generate_mermaid_gantt(task_order, original_names):
|
143 |
-
"""Generate Mermaid Gantt chart syntax."""
|
144 |
if not task_order:
|
145 |
-
return "gantt\n title Task Execution Timeline\n dateFormat YYYY-MM-DD\n section No Tasks\n No tasks to display : 2024-01-01,
|
146 |
|
147 |
-
|
148 |
-
|
|
|
|
|
149 |
|
150 |
gantt = "```mermaid\ngantt\n"
|
151 |
gantt += " title Task Execution Timeline\n"
|
152 |
-
gantt += " dateFormat YYYY-MM-DD\n"
|
153 |
gantt += " section Tasks\n"
|
154 |
|
155 |
-
|
156 |
for i, task in enumerate(task_order):
|
157 |
display_name = original_names.get(task, display_task_name(task))
|
158 |
# Clean task name for Mermaid (remove special chars)
|
159 |
clean_name = re.sub(r"[^a-zA-Z0-9\s]", "", display_name)
|
160 |
|
161 |
-
|
162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
|
164 |
-
|
|
|
|
|
165 |
|
166 |
gantt += "```"
|
167 |
return gantt
|
@@ -302,12 +312,17 @@ def format_dependencies_display(dependencies_list):
|
|
302 |
return display
|
303 |
|
304 |
|
305 |
-
def solve_dependencies(tasks_text, dependencies_list):
|
306 |
"""Solve the task ordering problem."""
|
307 |
tasks, task_original_names = parse_tasks(tasks_text)
|
308 |
|
|
|
|
|
|
|
|
|
|
|
309 |
if not tasks:
|
310 |
-
return "β Please enter some tasks!", "", "", "", ""
|
311 |
|
312 |
if not dependencies_list:
|
313 |
# No dependencies, just return tasks in alphabetical order
|
@@ -319,9 +334,12 @@ def solve_dependencies(tasks_text, dependencies_list):
|
|
319 |
output += f"{i}. {task_display}\n"
|
320 |
|
321 |
json_output = json.dumps(display_tasks, indent=2)
|
322 |
-
gantt = generate_mermaid_gantt(tasks, task_original_names)
|
323 |
flowchart = generate_mermaid_flowchart({}, tasks, task_original_names)
|
324 |
-
|
|
|
|
|
|
|
325 |
|
326 |
try:
|
327 |
dependencies, all_tasks, dep_original_names = parse_requirements(
|
@@ -368,10 +386,12 @@ def solve_dependencies(tasks_text, dependencies_list):
|
|
368 |
result_display.append(task_display)
|
369 |
|
370 |
json_output = json.dumps(result_display, indent=2)
|
371 |
-
gantt = generate_mermaid_gantt(result, all_original_names)
|
372 |
flowchart = generate_mermaid_flowchart(
|
373 |
dependencies, all_tasks, all_original_names
|
374 |
)
|
|
|
|
|
375 |
|
376 |
else:
|
377 |
# Try maximum subset
|
@@ -401,22 +421,26 @@ def solve_dependencies(tasks_text, dependencies_list):
|
|
401 |
output += f"β’ {task_display}\n"
|
402 |
|
403 |
json_output = json.dumps(result_display, indent=2)
|
404 |
-
gantt = generate_mermaid_gantt(result, all_original_names)
|
405 |
flowchart = generate_mermaid_flowchart(
|
406 |
dependencies, all_tasks, all_original_names
|
407 |
)
|
|
|
|
|
408 |
else:
|
409 |
output = "β **No solution found!** There might be complex circular dependencies."
|
410 |
json_output = "[]"
|
411 |
-
gantt = generate_mermaid_gantt([], all_original_names)
|
412 |
flowchart = generate_mermaid_flowchart(
|
413 |
dependencies, all_tasks, all_original_names
|
414 |
)
|
|
|
|
|
415 |
|
416 |
-
return output, dep_summary, json_output, gantt, flowchart
|
417 |
|
418 |
except Exception as e:
|
419 |
-
return f"β **Error:** {str(e)}", "", "", "", ""
|
420 |
|
421 |
|
422 |
# Example tasks
|
@@ -438,13 +462,16 @@ with gr.Blocks(title="Task Dependency Solver", theme=gr.themes.Soft()) as demo:
|
|
438 |
|
439 |
**How to use:**
|
440 |
1. Enter your tasks (one per line or comma-separated)
|
441 |
-
2.
|
442 |
-
3.
|
443 |
-
4. Click "
|
|
|
|
|
444 |
""")
|
445 |
|
446 |
# State to store current dependencies
|
447 |
dependencies_state = gr.State([])
|
|
|
448 |
|
449 |
with gr.Row():
|
450 |
with gr.Column(scale=1):
|
@@ -457,6 +484,18 @@ with gr.Blocks(title="Task Dependency Solver", theme=gr.themes.Soft()) as demo:
|
|
457 |
info="Case-insensitive: 'Sleep' and 'sleep' are treated the same",
|
458 |
)
|
459 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
460 |
gr.Markdown("### π Step 2: Build Dependencies")
|
461 |
task_dropdown = gr.Dropdown(
|
462 |
label="Select Task",
|
@@ -512,12 +551,14 @@ with gr.Blocks(title="Task Dependency Solver", theme=gr.themes.Soft()) as demo:
|
|
512 |
gantt_chart = gr.Markdown(
|
513 |
value="gantt\n title Task Execution Timeline\n dateFormat YYYY-MM-DD\n section Tasks\n Click Solve to see timeline : 2024-01-01, 1d"
|
514 |
)
|
|
|
515 |
|
516 |
with gr.Column(scale=1):
|
517 |
gr.Markdown("#### π Dependency Flowchart")
|
518 |
dependency_flowchart = gr.Markdown(
|
519 |
value="flowchart TD\n A[Click Solve to see dependencies]"
|
520 |
)
|
|
|
521 |
|
522 |
# Examples section
|
523 |
with gr.Accordion("π Quick Examples", open=False):
|
@@ -623,12 +664,52 @@ with gr.Blocks(title="Task Dependency Solver", theme=gr.themes.Soft()) as demo:
|
|
623 |
gr.CheckboxGroup(choices=[], value=[]),
|
624 |
)
|
625 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
626 |
# Wire up events
|
627 |
tasks_input.change(
|
628 |
fn=update_ui_after_tasks_change,
|
629 |
inputs=[tasks_input, dependencies_state],
|
630 |
outputs=[task_dropdown, requirements_checkbox, remove_deps],
|
631 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
632 |
|
633 |
add_btn.click(
|
634 |
fn=handle_add_dependencies,
|
@@ -656,13 +737,15 @@ with gr.Blocks(title="Task Dependency Solver", theme=gr.themes.Soft()) as demo:
|
|
656 |
|
657 |
solve_btn.click(
|
658 |
fn=solve_dependencies,
|
659 |
-
inputs=[tasks_input, dependencies_state],
|
660 |
outputs=[
|
661 |
result_output,
|
662 |
dep_analysis,
|
663 |
json_output,
|
664 |
gantt_chart,
|
665 |
dependency_flowchart,
|
|
|
|
|
666 |
],
|
667 |
)
|
668 |
|
@@ -688,6 +771,12 @@ with gr.Blocks(title="Task Dependency Solver", theme=gr.themes.Soft()) as demo:
|
|
688 |
inputs=[tasks_input, dependencies_state],
|
689 |
outputs=[task_dropdown, requirements_checkbox, remove_deps],
|
690 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
691 |
|
692 |
if __name__ == "__main__":
|
693 |
demo.launch()
|
|
|
139 |
return sorted(tasks), original_names
|
140 |
|
141 |
|
142 |
+
def generate_mermaid_gantt(task_order, original_names, durations=None, start_time=None):
|
143 |
+
"""Generate Mermaid Gantt chart syntax with minute granularity and selectable start time."""
|
144 |
if not task_order:
|
145 |
+
return "gantt\n title Task Execution Timeline\n dateFormat YYYY-MM-DD HH:mm\n section No Tasks\n No tasks to display : 2024-01-01 08:00, 1m"
|
146 |
|
147 |
+
if start_time is None:
|
148 |
+
start_date = datetime.now().replace(second=0, microsecond=0)
|
149 |
+
else:
|
150 |
+
start_date = start_time.replace(second=0, microsecond=0)
|
151 |
|
152 |
gantt = "```mermaid\ngantt\n"
|
153 |
gantt += " title Task Execution Timeline\n"
|
154 |
+
gantt += " dateFormat YYYY-MM-DD HH:mm\n"
|
155 |
gantt += " section Tasks\n"
|
156 |
|
157 |
+
current_dt = start_date
|
158 |
for i, task in enumerate(task_order):
|
159 |
display_name = original_names.get(task, display_task_name(task))
|
160 |
# Clean task name for Mermaid (remove special chars)
|
161 |
clean_name = re.sub(r"[^a-zA-Z0-9\s]", "", display_name)
|
162 |
|
163 |
+
# Get duration for this task in minutes (default 1m if not specified)
|
164 |
+
minutes = 1
|
165 |
+
if durations and task in durations:
|
166 |
+
minutes = durations[task]
|
167 |
+
elif durations is None:
|
168 |
+
minutes = 1
|
169 |
+
else:
|
170 |
+
minutes = durations.get(task, 1)
|
171 |
|
172 |
+
task_start = current_dt.strftime("%Y-%m-%d %H:%M")
|
173 |
+
gantt += f" {clean_name} : {task_start}, {minutes}m\n"
|
174 |
+
current_dt += timedelta(minutes=minutes)
|
175 |
|
176 |
gantt += "```"
|
177 |
return gantt
|
|
|
312 |
return display
|
313 |
|
314 |
|
315 |
+
def solve_dependencies(tasks_text, dependencies_list, durations_state=None):
|
316 |
"""Solve the task ordering problem."""
|
317 |
tasks, task_original_names = parse_tasks(tasks_text)
|
318 |
|
319 |
+
durations = durations_state or {}
|
320 |
+
# durations is a dict: {normalized_task_name: int_minutes}, may be string-int if loaded from input
|
321 |
+
# normalize keys and intify
|
322 |
+
durations = {normalize_task_name(k): int(v) for k, v in durations.items() if str(v).isdigit() and int(v) > 0}
|
323 |
+
|
324 |
if not tasks:
|
325 |
+
return "β Please enter some tasks!", "", "", "", "", "", ""
|
326 |
|
327 |
if not dependencies_list:
|
328 |
# No dependencies, just return tasks in alphabetical order
|
|
|
334 |
output += f"{i}. {task_display}\n"
|
335 |
|
336 |
json_output = json.dumps(display_tasks, indent=2)
|
337 |
+
gantt = generate_mermaid_gantt(tasks, task_original_names, durations=durations)
|
338 |
flowchart = generate_mermaid_flowchart({}, tasks, task_original_names)
|
339 |
+
# Provide raw for copy-paste, strip code block
|
340 |
+
gantt_raw = gantt.replace('```mermaid\n', '').rstrip('`\n')
|
341 |
+
flow_raw = flowchart.replace('```mermaid\n', '').rstrip('`\n')
|
342 |
+
return output, "", json_output, gantt, flowchart, gantt_raw, flow_raw
|
343 |
|
344 |
try:
|
345 |
dependencies, all_tasks, dep_original_names = parse_requirements(
|
|
|
386 |
result_display.append(task_display)
|
387 |
|
388 |
json_output = json.dumps(result_display, indent=2)
|
389 |
+
gantt = generate_mermaid_gantt(result, all_original_names, durations=durations)
|
390 |
flowchart = generate_mermaid_flowchart(
|
391 |
dependencies, all_tasks, all_original_names
|
392 |
)
|
393 |
+
gantt_raw = gantt.replace('```mermaid\n', '').rstrip('`\n')
|
394 |
+
flow_raw = flowchart.replace('```mermaid\n', '').rstrip('`\n')
|
395 |
|
396 |
else:
|
397 |
# Try maximum subset
|
|
|
421 |
output += f"β’ {task_display}\n"
|
422 |
|
423 |
json_output = json.dumps(result_display, indent=2)
|
424 |
+
gantt = generate_mermaid_gantt(result, all_original_names, durations=durations)
|
425 |
flowchart = generate_mermaid_flowchart(
|
426 |
dependencies, all_tasks, all_original_names
|
427 |
)
|
428 |
+
gantt_raw = gantt.replace('```mermaid\n', '').rstrip('`\n')
|
429 |
+
flow_raw = flowchart.replace('```mermaid\n', '').rstrip('`\n')
|
430 |
else:
|
431 |
output = "β **No solution found!** There might be complex circular dependencies."
|
432 |
json_output = "[]"
|
433 |
+
gantt = generate_mermaid_gantt([], all_original_names, durations=durations)
|
434 |
flowchart = generate_mermaid_flowchart(
|
435 |
dependencies, all_tasks, all_original_names
|
436 |
)
|
437 |
+
gantt_raw = gantt.replace('```mermaid\n', '').rstrip('`\n')
|
438 |
+
flow_raw = flowchart.replace('```mermaid\n', '').rstrip('`\n')
|
439 |
|
440 |
+
return output, dep_summary, json_output, gantt, flowchart, gantt_raw, flow_raw
|
441 |
|
442 |
except Exception as e:
|
443 |
+
return f"β **Error:** {str(e)}", "", "", "", "", "", ""
|
444 |
|
445 |
|
446 |
# Example tasks
|
|
|
462 |
|
463 |
**How to use:**
|
464 |
1. Enter your tasks (one per line or comma-separated)
|
465 |
+
2. (Optional) Enter the expected duration (in minutes) for each task.
|
466 |
+
3. Select a task, then select all its requirements at once
|
467 |
+
4. Click "Add Dependencies" to add them all
|
468 |
+
5. Click "Solve Dependencies" to get the optimal execution order with visualizations
|
469 |
+
6. You can copy and paste the generated Mermaid.js text below each diagram for use elsewhere!
|
470 |
""")
|
471 |
|
472 |
# State to store current dependencies
|
473 |
dependencies_state = gr.State([])
|
474 |
+
durations_state = gr.State({}) # dict: normalized_task -> int (user input durations)
|
475 |
|
476 |
with gr.Row():
|
477 |
with gr.Column(scale=1):
|
|
|
484 |
info="Case-insensitive: 'Sleep' and 'sleep' are treated the same",
|
485 |
)
|
486 |
|
487 |
+
gr.Markdown("### β±οΈ (Optional) Step 1.5: Set Task Durations (minutes)")
|
488 |
+
durations_table = gr.Dataframe(
|
489 |
+
headers=["Task", "Minutes"],
|
490 |
+
datatype=["str", "number"],
|
491 |
+
value=[],
|
492 |
+
col_count=(2, "fixed"),
|
493 |
+
row_count=(0, "dynamic"),
|
494 |
+
label="Set duration (minutes) for each listed task.",
|
495 |
+
interactive=True,
|
496 |
+
wrap=True
|
497 |
+
)
|
498 |
+
|
499 |
gr.Markdown("### π Step 2: Build Dependencies")
|
500 |
task_dropdown = gr.Dropdown(
|
501 |
label="Select Task",
|
|
|
551 |
gantt_chart = gr.Markdown(
|
552 |
value="gantt\n title Task Execution Timeline\n dateFormat YYYY-MM-DD\n section Tasks\n Click Solve to see timeline : 2024-01-01, 1d"
|
553 |
)
|
554 |
+
gantt_mermaid = gr.Code(label="Copy-paste Gantt Mermaid.js", value="", language="markdown")
|
555 |
|
556 |
with gr.Column(scale=1):
|
557 |
gr.Markdown("#### π Dependency Flowchart")
|
558 |
dependency_flowchart = gr.Markdown(
|
559 |
value="flowchart TD\n A[Click Solve to see dependencies]"
|
560 |
)
|
561 |
+
flow_mermaid = gr.Code(label="Copy-paste Flowchart Mermaid.js", value="", language="markdown")
|
562 |
|
563 |
# Examples section
|
564 |
with gr.Accordion("π Quick Examples", open=False):
|
|
|
664 |
gr.CheckboxGroup(choices=[], value=[]),
|
665 |
)
|
666 |
|
667 |
+
# Utility: update durations table to match tasks
|
668 |
+
def update_durations_table(tasks_text, old_duration_rows):
|
669 |
+
tasks, original_names = parse_tasks(tasks_text)
|
670 |
+
shown_task_set = set()
|
671 |
+
updated_rows = []
|
672 |
+
prev_map = {normalize_task_name(row[0]): row[1] for row in old_duration_rows if len(row) >= 2 and str(row[0]).strip()}
|
673 |
+
for task in tasks:
|
674 |
+
disp = original_names.get(task, display_task_name(task))
|
675 |
+
shown_task_set.add(task)
|
676 |
+
duration = prev_map.get(task, "")
|
677 |
+
updated_rows.append([disp, duration])
|
678 |
+
return updated_rows
|
679 |
+
|
680 |
+
# Converts Dataframe rows to durations dict (normalized)
|
681 |
+
def table_rows_to_durations(table_rows):
|
682 |
+
durations = {}
|
683 |
+
for row in table_rows:
|
684 |
+
if len(row) >= 2 and str(row[0]).strip() and str(row[1]).strip():
|
685 |
+
try:
|
686 |
+
val = int(float(row[1]))
|
687 |
+
if val > 0:
|
688 |
+
durations[normalize_task_name(row[0])] = val
|
689 |
+
except Exception:
|
690 |
+
continue
|
691 |
+
return durations
|
692 |
+
|
693 |
+
def update_durations_state(rows):
|
694 |
+
return table_rows_to_durations(rows)
|
695 |
+
|
696 |
# Wire up events
|
697 |
tasks_input.change(
|
698 |
fn=update_ui_after_tasks_change,
|
699 |
inputs=[tasks_input, dependencies_state],
|
700 |
outputs=[task_dropdown, requirements_checkbox, remove_deps],
|
701 |
)
|
702 |
+
# Keep durations table in sync
|
703 |
+
tasks_input.change(
|
704 |
+
fn=update_durations_table,
|
705 |
+
inputs=[tasks_input, durations_table],
|
706 |
+
outputs=[durations_table],
|
707 |
+
)
|
708 |
+
durations_table.change(
|
709 |
+
fn=update_durations_state,
|
710 |
+
inputs=[durations_table],
|
711 |
+
outputs=[durations_state],
|
712 |
+
)
|
713 |
|
714 |
add_btn.click(
|
715 |
fn=handle_add_dependencies,
|
|
|
737 |
|
738 |
solve_btn.click(
|
739 |
fn=solve_dependencies,
|
740 |
+
inputs=[tasks_input, dependencies_state, durations_state],
|
741 |
outputs=[
|
742 |
result_output,
|
743 |
dep_analysis,
|
744 |
json_output,
|
745 |
gantt_chart,
|
746 |
dependency_flowchart,
|
747 |
+
gantt_mermaid,
|
748 |
+
flow_mermaid,
|
749 |
],
|
750 |
)
|
751 |
|
|
|
771 |
inputs=[tasks_input, dependencies_state],
|
772 |
outputs=[task_dropdown, requirements_checkbox, remove_deps],
|
773 |
)
|
774 |
+
# Also set durations table on load
|
775 |
+
demo.load(
|
776 |
+
fn=update_durations_table,
|
777 |
+
inputs=[tasks_input, durations_table],
|
778 |
+
outputs=[durations_table],
|
779 |
+
)
|
780 |
|
781 |
if __name__ == "__main__":
|
782 |
demo.launch()
|
test_app.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
import pytest
|
2 |
-
from app import parse_requirements, parse_tasks, solve_all_tasks, solve_maximum_subset
|
|
|
3 |
|
4 |
def test_parse_requirements_with_spaces():
|
5 |
reqs = [
|
@@ -47,3 +48,14 @@ def test_solve_maximum_subset_cycle():
|
|
47 |
result = solve_maximum_subset(dependencies, all_tasks)
|
48 |
# Only d can run because a<->b<->c form a cycle
|
49 |
assert result == ["d"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import pytest
|
2 |
+
from app import parse_requirements, parse_tasks, solve_all_tasks, solve_maximum_subset, generate_mermaid_gantt
|
3 |
+
from datetime import datetime, timedelta
|
4 |
|
5 |
def test_parse_requirements_with_spaces():
|
6 |
reqs = [
|
|
|
48 |
result = solve_maximum_subset(dependencies, all_tasks)
|
49 |
# Only d can run because a<->b<->c form a cycle
|
50 |
assert result == ["d"]
|
51 |
+
|
52 |
+
def test_generate_mermaid_gantt_minutes():
|
53 |
+
order = ["a", "b", "c"]
|
54 |
+
names = {"a": "Alpha", "b": "Beta", "c": "Gamma"}
|
55 |
+
durations = {"a": 15, "b": 30, "c": 45}
|
56 |
+
base = datetime(2024, 1, 1, 8, 0)
|
57 |
+
gantt = generate_mermaid_gantt(order, names, durations=durations, start_time=base)
|
58 |
+
assert "dateFormat YYYY-MM-DD HH:mm" in gantt
|
59 |
+
assert ": 2024-01-01 08:00, 15m" in gantt
|
60 |
+
assert ": 2024-01-01 08:15, 30m" in gantt
|
61 |
+
assert ": 2024-01-01 08:45, 45m" in gantt
|