barakplasma commited on
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>

Files changed (3) hide show
  1. OpenCode.md +6 -5
  2. app.py +112 -23
  3. 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:** `python -m venv .venv && . .venv/bin/activate && pip install -r requirements.txt`
5
- - **Lint:** `.venv/bin/ruff check app.py`
6
- - **Format:** `.venv/bin/ruff format app.py`
7
- - **Run Gradio app:** `python app.py`
 
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`/`requirements.txt`.
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, 1d"
146
 
147
- # Start from today
148
- start_date = datetime.now()
 
 
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
- current_date = start_date
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
- task_start = current_date.strftime("%Y-%m-%d")
162
- current_date += timedelta(days=1)
 
 
 
 
 
 
163
 
164
- gantt += f" {clean_name} : {task_start}, 1d\n"
 
 
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
- return output, "", json_output, gantt, flowchart
 
 
 
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. Select a task, then select all its requirements at once
442
- 3. Click "Add Dependencies" to add them all
443
- 4. Click "Solve Dependencies" to get the optimal execution order with visualizations
 
 
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