labnotes 0.0.98__tar.gz → 0.0.100__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {labnotes-0.0.98 → labnotes-0.0.100}/PKG-INFO +1 -1
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/run_output.py +27 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/static/labnotes.css +56 -17
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/static/new-note.js +0 -2
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/static/note-edit.js +9 -16
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/static/note-editor.js +113 -69
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/static/run-output.js +29 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/open_tables.py +13 -0
- labnotes-0.0.100/src/labnotes/tables/read_blocks.py +17 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/templates/editor_imports.html +1 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/templates/run.html +2 -0
- labnotes-0.0.100/src/labnotes/tui/log_block.py +31 -0
- labnotes-0.0.100/src/labnotes/tui/log_blocks.py +55 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/run_app.py +5 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/run_screen.py +3 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/.gitignore +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/CHANGELOG.md +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/MIGRATIONS.md +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/README.md +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/pyproject.toml +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/context.md +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/daemon_app.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/describe_status.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/remote_start.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/watch_daemon.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/init.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/log.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/log_path.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/models.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/run_control.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/server.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/experiment_screen.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/leave_prompt.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/testing-conventions.toml +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.98 → labnotes-0.0.100}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labnotes
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.100
|
|
4
4
|
Summary: Browse experiment designs, notes, and run data from a folder.
|
|
5
5
|
Project-URL: Repository, https://github.com/thekevinscott/labnotes
|
|
6
6
|
Author-email: Kevin Scott <me@thekevinscott.com>
|
|
@@ -43,8 +43,30 @@ async def run_output(
|
|
|
43
43
|
last, mid_line = stream, not text.endswith("\n")
|
|
44
44
|
yield f"data: {json.dumps({'stream': stream, 'text': text})}\n\n"
|
|
45
45
|
|
|
46
|
+
blocks_read = 0
|
|
47
|
+
|
|
48
|
+
def read_blocks() -> Iterator[str]:
|
|
49
|
+
nonlocal blocks_read
|
|
50
|
+
path = folder / "log.jsonl"
|
|
51
|
+
if not path.exists():
|
|
52
|
+
return
|
|
53
|
+
with path.open("rb") as log:
|
|
54
|
+
log.seek(blocks_read)
|
|
55
|
+
data = log.read()
|
|
56
|
+
whole = data[: data.rfind(b"\n") + 1]
|
|
57
|
+
blocks_read += len(whole)
|
|
58
|
+
for line in whole.splitlines():
|
|
59
|
+
try:
|
|
60
|
+
record = json.loads(line)
|
|
61
|
+
except json.JSONDecodeError:
|
|
62
|
+
continue
|
|
63
|
+
if isinstance(record, dict):
|
|
64
|
+
yield f"event: block\ndata: {json.dumps(record)}\n\n"
|
|
65
|
+
|
|
46
66
|
for message in read(STREAMS):
|
|
47
67
|
yield message
|
|
68
|
+
for message in read_blocks():
|
|
69
|
+
yield message
|
|
48
70
|
exit_code = run["exit_code"]
|
|
49
71
|
if run["ended"] is None:
|
|
50
72
|
async for event in events:
|
|
@@ -54,6 +76,9 @@ async def run_output(
|
|
|
54
76
|
if event.table == "logs" and row["run"] == run_id:
|
|
55
77
|
for message in read([row["stream"]]):
|
|
56
78
|
yield message
|
|
79
|
+
elif event.table == "blocks" and row["run"] == run_id:
|
|
80
|
+
for message in read_blocks():
|
|
81
|
+
yield message
|
|
57
82
|
elif (
|
|
58
83
|
event.table == "runs"
|
|
59
84
|
and row["id"] == run_id
|
|
@@ -63,4 +88,6 @@ async def run_output(
|
|
|
63
88
|
break
|
|
64
89
|
for message in read(STREAMS):
|
|
65
90
|
yield message
|
|
91
|
+
for message in read_blocks():
|
|
92
|
+
yield message
|
|
66
93
|
yield f"event: end\ndata: {json.dumps(exit_code)}\n\n"
|
|
@@ -302,6 +302,13 @@ h3 {
|
|
|
302
302
|
/* Run output */
|
|
303
303
|
.run-state.good { color: var(--good); }
|
|
304
304
|
.run-state.bad { color: var(--bad); }
|
|
305
|
+
.run-blocks { background: var(--surface); border: 1px solid var(--rule); border-radius: 6px; margin-bottom: 1rem; padding: 0.5rem 0; max-height: 50vh; overflow: auto; }
|
|
306
|
+
.run-blocks[hidden] { display: none; }
|
|
307
|
+
.log-block { display: block; width: 100%; padding: 0.2rem 1.25rem; border: 0; background: none; color: inherit; font: inherit; font-family: var(--mono); font-size: 0.82rem; text-align: left; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
308
|
+
.log-block::before { content: "▸ "; color: var(--ink-3); }
|
|
309
|
+
.log-block[aria-expanded="true"] { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
310
|
+
.log-block[aria-expanded="true"]::before { content: "▾ "; }
|
|
311
|
+
.log-block:hover { background: color-mix(in srgb, var(--accent) 4%, transparent); }
|
|
305
312
|
.run-log { background: var(--surface); border: 1px solid var(--rule); border-radius: 6px; }
|
|
306
313
|
.run-log header { display: flex; gap: 1rem; padding: 0.4rem 1.25rem; border-bottom: 1px solid var(--rule); font-family: var(--mono); font-size: 0.75rem; color: var(--ink-3); }
|
|
307
314
|
.run-log header span::before { content: "■ "; }
|
|
@@ -418,7 +425,7 @@ h3 {
|
|
|
418
425
|
top: 0.5rem;
|
|
419
426
|
right: 0.5rem;
|
|
420
427
|
}
|
|
421
|
-
.cell .run, .note-name .restart {
|
|
428
|
+
.cell .run, .editor .run, .note-name .restart {
|
|
422
429
|
font: inherit;
|
|
423
430
|
font-family: var(--sans);
|
|
424
431
|
font-size: 0.75rem;
|
|
@@ -429,8 +436,8 @@ h3 {
|
|
|
429
436
|
color: var(--ink-2);
|
|
430
437
|
cursor: pointer;
|
|
431
438
|
}
|
|
432
|
-
.cell .run:hover, .note-name .restart:hover { color: var(--accent); border-color: var(--accent); }
|
|
433
|
-
.cell .run:disabled, .note-name .restart:disabled { cursor: progress; opacity: 0.6; }
|
|
439
|
+
.cell .run:hover, .editor .run:hover, .note-name .restart:hover { color: var(--accent); border-color: var(--accent); }
|
|
440
|
+
.cell .run:disabled, .editor .run:disabled, .note-name .restart:disabled { cursor: progress; opacity: 0.6; }
|
|
434
441
|
.note-name:has(.restart) { display: flex; align-items: center; justify-content: flex-end; padding-top: 0.45rem; padding-bottom: 0.45rem; }
|
|
435
442
|
.cell-output { display: block; }
|
|
436
443
|
.cell-output:not(:empty) { margin-top: 0.4rem; padding-left: 0.9rem; border-left: 2px solid var(--accent-soft); overflow-x: auto; }
|
|
@@ -438,25 +445,57 @@ h3 {
|
|
|
438
445
|
.cell-output .stderr, .cell-output .error { color: var(--bad); }
|
|
439
446
|
.cell-output img, .cell-output svg { max-width: 100%; height: auto; }
|
|
440
447
|
|
|
441
|
-
/* Editing a note in place */
|
|
442
|
-
.prose.editable { cursor: text; }
|
|
448
|
+
/* Editing a note in place: CodeMirror styled to match .prose */
|
|
443
449
|
.edit-status { margin: 0; padding: 0.45rem 1.75rem; font-family: var(--sans); font-size: 0.8rem; color: var(--ink-3); border-bottom: 1px solid var(--rule); }
|
|
444
450
|
.editor { padding: 0.5rem 1.75rem 1.5rem; max-width: 46rem; }
|
|
445
451
|
.editor .cm-editor.cm-focused { outline: none; }
|
|
446
|
-
.editor .cm-content { font-family: var(--serif); font-size: 1.06rem; line-height: 1.68; caret-color: var(--ink); }
|
|
452
|
+
.editor .cm-content { padding: 0.8em 0; font-family: var(--serif); font-size: 1.06rem; line-height: 1.68; caret-color: var(--ink); }
|
|
447
453
|
.editor .cm-line { padding: 0; }
|
|
448
|
-
.editor .
|
|
449
|
-
.editor .
|
|
450
|
-
.editor .
|
|
451
|
-
.editor .
|
|
452
|
-
.editor .
|
|
453
|
-
.editor .
|
|
454
|
-
.editor .
|
|
455
|
-
.editor .
|
|
456
|
-
.editor .
|
|
457
|
-
.editor .
|
|
454
|
+
.editor .cm-line:has(> br:only-child):not(.cm-atomic-fenced-code) { line-height: 0.8em; }
|
|
455
|
+
.editor .cm-atomic-h1, .editor .cm-atomic-h2, .editor .cm-atomic-h3, .editor .cm-atomic-h4, .editor .cm-atomic-h5, .editor .cm-atomic-h6 { font-weight: 600; letter-spacing: -0.01em; line-height: 1.3; padding: 1em 0 0.5em; }
|
|
456
|
+
.editor .cm-line.cm-atomic-h1 { font-size: 1.6rem; }
|
|
457
|
+
.editor .cm-line.cm-atomic-h2 { font-size: 1.3rem; }
|
|
458
|
+
.editor .cm-line.cm-atomic-h3 { font-size: 1.1rem; }
|
|
459
|
+
.editor .cm-line.cm-atomic-h4, .editor .cm-line.cm-atomic-h5, .editor .cm-line.cm-atomic-h6 { font-size: 1rem; }
|
|
460
|
+
.editor .cm-atomic-strong { font-weight: 700; }
|
|
461
|
+
.editor .cm-atomic-em { font-style: italic; }
|
|
462
|
+
.editor .cm-atomic-strike { text-decoration: line-through; }
|
|
463
|
+
.editor .cm-atomic-inline-code { font-family: var(--mono); font-size: 0.86em; background: var(--chip); padding: 0.1em 0.35em; border-radius: 4px; }
|
|
464
|
+
.editor .cm-atomic-link { text-decoration: underline; text-decoration-color: var(--rule-strong); text-underline-offset: 3px; }
|
|
465
|
+
.editor .cm-atomic-link::after {
|
|
466
|
+
content: "↗";
|
|
467
|
+
margin-left: 0.15em;
|
|
468
|
+
font-family: var(--sans);
|
|
469
|
+
font-size: 0.75em;
|
|
470
|
+
color: var(--ink-3);
|
|
471
|
+
cursor: pointer;
|
|
472
|
+
}
|
|
473
|
+
.editor .cm-atomic-link:hover::after { color: var(--accent); }
|
|
474
|
+
.editor .cm-line.cm-atomic-blockquote { padding-left: 1rem; border-left: 2px solid var(--rule-strong); color: var(--ink-2); }
|
|
475
|
+
.editor .cm-atomic-list-marker { display: inline-block; width: 0.9em; margin-right: 0.3em; text-align: right; }
|
|
476
|
+
.editor .cm-atomic-list-marker, .editor .cm-atomic-list-marker .md-mark { color: var(--ink); }
|
|
477
|
+
.editor .cm-line.cm-atomic-hr { position: relative; }
|
|
478
|
+
.editor .cm-line.cm-atomic-hr::after { content: ""; position: absolute; left: 0; right: 0; top: 50%; border-top: 1px solid var(--rule); }
|
|
479
|
+
.editor .cm-line.cm-atomic-fenced-code {
|
|
480
|
+
position: relative;
|
|
481
|
+
font-family: var(--mono);
|
|
482
|
+
font-size: 0.8rem;
|
|
483
|
+
line-height: 1.5;
|
|
484
|
+
background: var(--paper);
|
|
485
|
+
padding: 0 1rem;
|
|
486
|
+
border: solid var(--rule);
|
|
487
|
+
border-width: 0 1px;
|
|
488
|
+
}
|
|
489
|
+
.editor .cm-line:not(.cm-atomic-fenced-code) + .cm-atomic-fenced-code, .editor .cm-atomic-fenced-code:first-child { border-top-width: 1px; border-radius: 6px 6px 0 0; }
|
|
490
|
+
.editor .cm-atomic-fenced-code:not(:has(+ .cm-atomic-fenced-code)) { border-bottom-width: 1px; border-radius: 0 0 6px 6px; }
|
|
491
|
+
.editor .cm-line:not(.cm-atomic-fenced-code) + .cm-atomic-fenced-code:not(:has(+ .cm-atomic-fenced-code)) { border-radius: 6px; }
|
|
492
|
+
.editor .cm-atomic-fenced-selection { background: var(--accent-soft); }
|
|
493
|
+
.editor .run { position: absolute; z-index: 1; top: 0.5rem; right: 0.5rem; }
|
|
494
|
+
/* CodeMirror measures block widgets without their margins. */
|
|
495
|
+
.editor .cell-below { display: flow-root; padding-bottom: 0.8em; }
|
|
496
|
+
.editor .cell-output pre { margin: 0; font-size: 0.8rem; line-height: 1.5; }
|
|
497
|
+
.editor .cm-line .cm-widgetBuffer { vertical-align: top; }
|
|
458
498
|
.editor .md-mark { color: var(--ink-3); }
|
|
459
|
-
.editor .md-code-line { font-family: var(--mono); font-size: 0.8rem; line-height: 1.5; background: var(--paper); padding: 0 1rem; }
|
|
460
499
|
.editor .py-keyword { color: var(--accent); }
|
|
461
500
|
.editor .py-string { color: var(--good); }
|
|
462
501
|
.editor .py-comment { color: var(--ink-3); font-style: italic; }
|
|
@@ -22,8 +22,6 @@ function show(note, button) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
for (const button of document.querySelectorAll(".new-note")) {
|
|
25
|
-
// An open editor closes, and reloads the page, on a mousedown outside it.
|
|
26
|
-
button.addEventListener("mousedown", (event) => event.stopPropagation());
|
|
27
25
|
button.addEventListener("click", async () => {
|
|
28
26
|
button.disabled = true;
|
|
29
27
|
const response = await fetch(button.dataset.url, {
|
|
@@ -1,35 +1,28 @@
|
|
|
1
1
|
let detach = () => {};
|
|
2
2
|
|
|
3
|
-
export function attach(note,
|
|
3
|
+
export function attach(note, focus) {
|
|
4
4
|
detach();
|
|
5
5
|
const prose = note.querySelector(".prose");
|
|
6
6
|
const status = note.querySelector(".edit-status");
|
|
7
|
-
const editor = import(note.dataset.editor);
|
|
8
7
|
const source = JSON.parse(note.querySelector(".note-source").textContent);
|
|
9
8
|
let editing = null;
|
|
10
9
|
let attached = true;
|
|
11
10
|
|
|
12
|
-
editor.then(
|
|
13
|
-
() =>
|
|
11
|
+
import(note.dataset.editor).then(
|
|
12
|
+
({ edit }) => {
|
|
13
|
+
if (!attached) return;
|
|
14
|
+
editing = edit(note, prose, status);
|
|
15
|
+
if (focus) editing.view.focus();
|
|
16
|
+
},
|
|
14
17
|
() => {},
|
|
15
18
|
);
|
|
16
19
|
|
|
17
|
-
const open = async (event) => {
|
|
18
|
-
if (event.target.closest("a, button, output") || getSelection().toString()) return;
|
|
19
|
-
const { edit } = await editor;
|
|
20
|
-
if (editing || !attached) return;
|
|
21
|
-
prose.removeEventListener("click", open);
|
|
22
|
-
editing = edit(note, prose, status);
|
|
23
|
-
};
|
|
24
|
-
prose.addEventListener("click", open);
|
|
25
|
-
if (openNow) open({ target: prose });
|
|
26
|
-
|
|
27
20
|
const changes = new EventSource(note.dataset.changesUrl);
|
|
28
21
|
changes.addEventListener("message", (event) => {
|
|
29
22
|
if (JSON.parse(event.data) !== note.dataset.note) return;
|
|
30
23
|
if (editing) return editing.pull();
|
|
31
|
-
//
|
|
32
|
-
//
|
|
24
|
+
// Without the editor the note is rendered HTML. A touch or a duplicate
|
|
25
|
+
// event leaves the text alone, and reloading then is wasted.
|
|
33
26
|
fetch(note.dataset.saveUrl, { cache: "no-store" })
|
|
34
27
|
.then((response) => response.text())
|
|
35
28
|
.then((text) => {
|
|
@@ -4,12 +4,14 @@ import {
|
|
|
4
4
|
HighlightStyle,
|
|
5
5
|
Language,
|
|
6
6
|
defineLanguageFacet,
|
|
7
|
+
ensureSyntaxTree,
|
|
7
8
|
languageDataProp,
|
|
8
9
|
syntaxHighlighting,
|
|
9
10
|
syntaxTree,
|
|
10
11
|
} from "@codemirror/language";
|
|
11
|
-
import { EditorState,
|
|
12
|
-
import { Decoration, EditorView,
|
|
12
|
+
import { EditorState, StateField } from "@codemirror/state";
|
|
13
|
+
import { Decoration, EditorView, WidgetType, keymap } from "@codemirror/view";
|
|
14
|
+
import { inlinePreview } from "@atomic-editor/editor/inline-preview";
|
|
13
15
|
import { tags } from "@lezer/highlight";
|
|
14
16
|
import { GFM, parseCode, parser } from "@lezer/markdown";
|
|
15
17
|
import { DIFF_DELETE, DIFF_EQUAL, diff_match_patch } from "diff-match-patch";
|
|
@@ -31,16 +33,6 @@ const markdown = new Language(
|
|
|
31
33
|
);
|
|
32
34
|
|
|
33
35
|
const style = HighlightStyle.define([
|
|
34
|
-
{ tag: tags.heading1, class: "md-h1" },
|
|
35
|
-
{ tag: tags.heading2, class: "md-h2" },
|
|
36
|
-
{ tag: tags.heading3, class: "md-h3" },
|
|
37
|
-
{ tag: [tags.heading4, tags.heading5, tags.heading6], class: "md-h4" },
|
|
38
|
-
{ tag: tags.strong, class: "md-strong" },
|
|
39
|
-
{ tag: tags.emphasis, class: "md-em" },
|
|
40
|
-
{ tag: tags.strikethrough, class: "md-strike" },
|
|
41
|
-
{ tag: [tags.link, tags.url], class: "md-link" },
|
|
42
|
-
{ tag: tags.monospace, class: "md-code" },
|
|
43
|
-
{ tag: tags.quote, class: "md-quote" },
|
|
44
36
|
{ tag: [tags.processingInstruction, tags.contentSeparator], class: "md-mark" },
|
|
45
37
|
{ tag: [tags.keyword, tags.operatorKeyword, tags.bool, tags.null], class: "py-keyword" },
|
|
46
38
|
{ tag: [tags.string, tags.special(tags.string)], class: "py-string" },
|
|
@@ -49,54 +41,86 @@ const style = HighlightStyle.define([
|
|
|
49
41
|
{ tag: [tags.function(tags.variableName), tags.definition(tags.variableName)], class: "py-name" },
|
|
50
42
|
]);
|
|
51
43
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
function decorate(view) {
|
|
58
|
-
const builder = new RangeSetBuilder();
|
|
59
|
-
const { state } = view;
|
|
60
|
-
const active = new Set();
|
|
61
|
-
for (const range of state.selection.ranges) {
|
|
62
|
-
for (let n = state.doc.lineAt(range.from).number; n <= state.doc.lineAt(range.to).number; n++) active.add(n);
|
|
44
|
+
class RunButton extends WidgetType {
|
|
45
|
+
constructor(block, run) {
|
|
46
|
+
super();
|
|
47
|
+
this.block = block;
|
|
48
|
+
this.run = run;
|
|
63
49
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (!HIDDEN.has(node.name) || active.has(state.doc.lineAt(node.from).number)) return;
|
|
78
|
-
let end = node.to;
|
|
79
|
-
if (node.name === "HeaderMark" && state.doc.sliceString(end, end + 1) === " ") end += 1;
|
|
80
|
-
builder.add(node.from, end, hide);
|
|
81
|
-
},
|
|
82
|
-
});
|
|
50
|
+
eq(other) {
|
|
51
|
+
return other.block === this.block;
|
|
52
|
+
}
|
|
53
|
+
toDOM() {
|
|
54
|
+
const button = document.createElement("button");
|
|
55
|
+
button.type = "button";
|
|
56
|
+
button.className = "run";
|
|
57
|
+
button.textContent = "Run";
|
|
58
|
+
button.addEventListener("click", () => this.run(this.block, button));
|
|
59
|
+
return button;
|
|
60
|
+
}
|
|
61
|
+
ignoreEvent() {
|
|
62
|
+
return true;
|
|
83
63
|
}
|
|
84
|
-
return builder.finish();
|
|
85
64
|
}
|
|
86
65
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
66
|
+
class CellOutput extends WidgetType {
|
|
67
|
+
constructor(block, outputs) {
|
|
68
|
+
super();
|
|
69
|
+
this.block = block;
|
|
70
|
+
this.outputs = outputs;
|
|
71
|
+
}
|
|
72
|
+
eq(other) {
|
|
73
|
+
return other.block === this.block;
|
|
74
|
+
}
|
|
75
|
+
toDOM() {
|
|
76
|
+
const cell = document.createElement("div");
|
|
77
|
+
cell.className = "cell-below";
|
|
78
|
+
if (!this.outputs.has(this.block)) {
|
|
79
|
+
const output = document.createElement("output");
|
|
80
|
+
output.className = "cell-output";
|
|
81
|
+
this.outputs.set(this.block, output);
|
|
91
82
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
83
|
+
cell.append(this.outputs.get(this.block));
|
|
84
|
+
return cell;
|
|
85
|
+
}
|
|
86
|
+
ignoreEvent() {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Numbered in document order, as python_blocks numbers them on the server.
|
|
92
|
+
function cellWidgets(state, run, outputs) {
|
|
93
|
+
const widgets = [];
|
|
94
|
+
let block = 0;
|
|
95
|
+
const tree = ensureSyntaxTree(state, state.doc.length, 200) ?? syntaxTree(state);
|
|
96
|
+
tree.iterate({
|
|
97
|
+
enter(node) {
|
|
98
|
+
if (node.name !== "FencedCode") return;
|
|
99
|
+
const info = node.node.getChild("CodeInfo");
|
|
100
|
+
if (info && state.doc.sliceString(info.from, info.to).split(/\s/)[0] === "python") {
|
|
101
|
+
const open = state.doc.lineAt(node.from);
|
|
102
|
+
widgets.push(Decoration.widget({ widget: new RunButton(block, run), side: 1 }).range(open.to));
|
|
103
|
+
widgets.push(
|
|
104
|
+
Decoration.widget({ widget: new CellOutput(block, outputs), block: true, side: 1 }).range(node.to),
|
|
105
|
+
);
|
|
106
|
+
block += 1;
|
|
95
107
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
);
|
|
108
|
+
return false;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
return Decoration.set(widgets, true);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function cells(run, outputs) {
|
|
115
|
+
return StateField.define({
|
|
116
|
+
create: (state) => cellWidgets(state, run, outputs),
|
|
117
|
+
update(widgets, tr) {
|
|
118
|
+
if (!tr.docChanged && syntaxTree(tr.state) === syntaxTree(tr.startState)) return widgets;
|
|
119
|
+
return cellWidgets(tr.state, run, outputs);
|
|
120
|
+
},
|
|
121
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
100
124
|
|
|
101
125
|
function show(status, message) {
|
|
102
126
|
status.textContent = message;
|
|
@@ -196,15 +220,33 @@ export function edit(note, prose, status) {
|
|
|
196
220
|
|
|
197
221
|
const save = () => run(push);
|
|
198
222
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
223
|
+
// The rendered note's outputs move into the editor, so a run started
|
|
224
|
+
// before the editor loaded still shows its result.
|
|
225
|
+
const outputs = new Map();
|
|
226
|
+
for (const cell of prose.querySelectorAll(".cell")) {
|
|
227
|
+
outputs.set(Number(cell.dataset.block), cell.querySelector(".cell-output"));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The server runs the block from the file, so unsaved edits go first.
|
|
231
|
+
const runCell = async (block, button) => {
|
|
232
|
+
button.disabled = true;
|
|
233
|
+
button.textContent = "Running";
|
|
234
|
+
try {
|
|
235
|
+
await save();
|
|
236
|
+
const response = await fetch(note.dataset.runUrl, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: { "Content-Type": "application/json" },
|
|
239
|
+
body: JSON.stringify({ block }),
|
|
240
|
+
});
|
|
241
|
+
outputs.get(block).innerHTML = response.ok
|
|
242
|
+
? await response.text()
|
|
243
|
+
: `<pre class="error">${response.status} ${response.statusText}</pre>`;
|
|
244
|
+
view.requestMeasure();
|
|
245
|
+
} finally {
|
|
246
|
+
button.disabled = false;
|
|
247
|
+
button.textContent = "Run";
|
|
248
|
+
}
|
|
206
249
|
};
|
|
207
|
-
document.addEventListener("mousedown", outside);
|
|
208
250
|
|
|
209
251
|
const view = new EditorView({
|
|
210
252
|
parent: host,
|
|
@@ -212,10 +254,15 @@ export function edit(note, prose, status) {
|
|
|
212
254
|
doc: base,
|
|
213
255
|
extensions: [
|
|
214
256
|
history(),
|
|
215
|
-
keymap.of([
|
|
257
|
+
keymap.of([
|
|
258
|
+
{ key: "Escape", run: () => (view.contentDOM.blur(), true) },
|
|
259
|
+
...defaultKeymap,
|
|
260
|
+
...historyKeymap,
|
|
261
|
+
]),
|
|
216
262
|
markdown,
|
|
217
263
|
syntaxHighlighting(style),
|
|
218
|
-
|
|
264
|
+
inlinePreview({ onLinkClick: (url) => location.assign(new URL(url, location.href)) }),
|
|
265
|
+
cells(runCell, outputs),
|
|
219
266
|
EditorView.lineWrapping,
|
|
220
267
|
EditorView.contentAttributes.of({ "aria-label": "Note" }),
|
|
221
268
|
EditorView.updateListener.of((update) => {
|
|
@@ -227,12 +274,9 @@ export function edit(note, prose, status) {
|
|
|
227
274
|
],
|
|
228
275
|
}),
|
|
229
276
|
});
|
|
230
|
-
view.focus();
|
|
231
277
|
return {
|
|
278
|
+
view,
|
|
232
279
|
pull: () => run(pull),
|
|
233
|
-
detach: () =>
|
|
234
|
-
document.removeEventListener("mousedown", outside);
|
|
235
|
-
save();
|
|
236
|
-
},
|
|
280
|
+
detach: () => save(),
|
|
237
281
|
};
|
|
238
282
|
}
|
|
@@ -4,12 +4,41 @@ const output = document.querySelector(".run-output");
|
|
|
4
4
|
const state = document.querySelector(".run-state");
|
|
5
5
|
const source = new EventSource(output.dataset.url);
|
|
6
6
|
|
|
7
|
+
const blocks = document.querySelector(".run-blocks");
|
|
8
|
+
|
|
7
9
|
let parsers = {};
|
|
10
|
+
let streaming = new Map();
|
|
8
11
|
|
|
9
12
|
// The server resends the whole log on each connection, so a reconnect starts clean.
|
|
10
13
|
source.addEventListener("open", () => {
|
|
11
14
|
output.replaceChildren();
|
|
12
15
|
parsers = {};
|
|
16
|
+
blocks.replaceChildren();
|
|
17
|
+
blocks.hidden = true;
|
|
18
|
+
streaming = new Map();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
source.addEventListener("block", (event) => {
|
|
22
|
+
const { id, phase, content = "" } = JSON.parse(event.data);
|
|
23
|
+
const open = streaming.get(id);
|
|
24
|
+
if (open && (phase === "delta" || phase === "stop")) {
|
|
25
|
+
open.textContent += content;
|
|
26
|
+
if (phase === "stop") streaming.delete(id);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (phase === "stop") return;
|
|
30
|
+
const line = document.createElement("button");
|
|
31
|
+
line.type = "button";
|
|
32
|
+
line.className = "log-block";
|
|
33
|
+
line.setAttribute("aria-expanded", "false");
|
|
34
|
+
line.textContent = content;
|
|
35
|
+
line.addEventListener("click", () => {
|
|
36
|
+
const expanded = line.getAttribute("aria-expanded") === "true";
|
|
37
|
+
line.setAttribute("aria-expanded", String(!expanded));
|
|
38
|
+
});
|
|
39
|
+
if (phase === "start" || phase === "delta") streaming.set(id, line);
|
|
40
|
+
blocks.append(line);
|
|
41
|
+
blocks.hidden = false;
|
|
13
42
|
});
|
|
14
43
|
|
|
15
44
|
source.addEventListener("message", (event) => {
|
|
@@ -4,6 +4,7 @@ from dirsql import DirSQL, Table
|
|
|
4
4
|
|
|
5
5
|
from labnotes.models import LabnotesConfig
|
|
6
6
|
from labnotes.tables.path_glob import path_glob
|
|
7
|
+
from labnotes.tables.read_blocks import read_blocks
|
|
7
8
|
from labnotes.tables.read_experiment import read_experiment
|
|
8
9
|
from labnotes.tables.read_file import read_file
|
|
9
10
|
from labnotes.tables.read_log import read_log
|
|
@@ -48,6 +49,12 @@ CREATE TABLE logs (
|
|
|
48
49
|
PRIMARY KEY (experiment, run, stream)
|
|
49
50
|
)
|
|
50
51
|
"""
|
|
52
|
+
BLOCKS = """
|
|
53
|
+
CREATE TABLE blocks (
|
|
54
|
+
experiment TEXT, run TEXT, size INTEGER,
|
|
55
|
+
PRIMARY KEY (experiment, run)
|
|
56
|
+
)
|
|
57
|
+
"""
|
|
51
58
|
|
|
52
59
|
|
|
53
60
|
def open_tables(config: LabnotesConfig) -> DirSQL:
|
|
@@ -73,6 +80,12 @@ def open_tables(config: LabnotesConfig) -> DirSQL:
|
|
|
73
80
|
glob=path_glob(f"{config.runs}/std*.log"),
|
|
74
81
|
on_file=partial(read_log, root, config.runs),
|
|
75
82
|
),
|
|
83
|
+
Table(
|
|
84
|
+
name="blocks",
|
|
85
|
+
ddl=BLOCKS,
|
|
86
|
+
glob=path_glob(f"{config.runs}/log.jsonl"),
|
|
87
|
+
on_file=partial(read_blocks, root, config.runs),
|
|
88
|
+
),
|
|
76
89
|
Table(
|
|
77
90
|
name="experiment_configs",
|
|
78
91
|
ddl=EXPERIMENTS,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from labnotes.tables.path_pattern import path_pattern
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def read_blocks(root: Path, template: str, path: str) -> list[dict[str, object]]:
|
|
7
|
+
relative = Path(path).relative_to(root).as_posix()
|
|
8
|
+
found = path_pattern(f"{template}/log.jsonl").fullmatch(relative)
|
|
9
|
+
if found is None:
|
|
10
|
+
return []
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
"experiment": found["experiment"],
|
|
14
|
+
"run": found["run"],
|
|
15
|
+
"size": Path(path).stat().st_size,
|
|
16
|
+
}
|
|
17
|
+
]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<script type="importmap">
|
|
3
3
|
{
|
|
4
4
|
"imports": {
|
|
5
|
+
"@atomic-editor/editor/inline-preview": "https://esm.sh/*@atomic-editor/editor@0.6.2/dist/inline-preview.js",
|
|
5
6
|
"@codemirror/autocomplete": "https://esm.sh/*@codemirror/autocomplete@6.20.4",
|
|
6
7
|
"@codemirror/commands": "https://esm.sh/*@codemirror/commands@6.11.1",
|
|
7
8
|
"@codemirror/lang-python": "https://esm.sh/*@codemirror/lang-python@6.2.1",
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
</p>
|
|
12
12
|
</section>
|
|
13
13
|
|
|
14
|
+
<section class="run-blocks" hidden></section>
|
|
15
|
+
|
|
14
16
|
<section class="run-log">
|
|
15
17
|
<header><span class="stdout">stdout</span><span class="stderr">stderr</span></header>
|
|
16
18
|
<pre class="run-output" data-url="{{ url_for('output', name=run.experiment, run=run.id) }}"></pre>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from rich.text import Text
|
|
2
|
+
from textual.events import Click
|
|
3
|
+
from textual.widgets import Static
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LogBlock(Static):
|
|
7
|
+
DEFAULT_CSS = """
|
|
8
|
+
LogBlock { height: auto; }
|
|
9
|
+
LogBlock.-collapsed { height: 1; text-wrap: nowrap; text-overflow: ellipsis; }
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, body: str) -> None:
|
|
13
|
+
super().__init__(classes="-collapsed")
|
|
14
|
+
self.body = body
|
|
15
|
+
self.expanded = False
|
|
16
|
+
self.show()
|
|
17
|
+
|
|
18
|
+
def extend(self, text: str) -> None:
|
|
19
|
+
self.body += text
|
|
20
|
+
self.show()
|
|
21
|
+
|
|
22
|
+
def on_click(self, _: Click) -> None:
|
|
23
|
+
self.expanded = not self.expanded
|
|
24
|
+
self.set_class(not self.expanded, "-collapsed")
|
|
25
|
+
self.show()
|
|
26
|
+
|
|
27
|
+
def show(self) -> None:
|
|
28
|
+
if self.expanded:
|
|
29
|
+
self.update(Text(f"▾ {self.body}"))
|
|
30
|
+
else:
|
|
31
|
+
self.update(Text(f"▸ {self.body}".replace("\n", " ")))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from textual.containers import VerticalScroll
|
|
5
|
+
|
|
6
|
+
from labnotes.tui.log_block import LogBlock
|
|
7
|
+
|
|
8
|
+
STREAMING = ("start", "delta")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LogBlocks(VerticalScroll):
|
|
12
|
+
DEFAULT_CSS = "LogBlocks { height: auto; max-height: 50%; }"
|
|
13
|
+
|
|
14
|
+
def __init__(self, id: str | None = None) -> None:
|
|
15
|
+
super().__init__(id=id)
|
|
16
|
+
self.reset()
|
|
17
|
+
|
|
18
|
+
def reset(self) -> None:
|
|
19
|
+
self.read_to = 0
|
|
20
|
+
self.streaming: dict[object, LogBlock] = {}
|
|
21
|
+
self.remove_children()
|
|
22
|
+
self.display = False
|
|
23
|
+
|
|
24
|
+
def follow(self, folder: Path) -> None:
|
|
25
|
+
path = folder / "log.jsonl"
|
|
26
|
+
if not path.exists():
|
|
27
|
+
return
|
|
28
|
+
with path.open("rb") as log:
|
|
29
|
+
log.seek(self.read_to)
|
|
30
|
+
data = log.read()
|
|
31
|
+
complete = data[: data.rfind(b"\n") + 1]
|
|
32
|
+
self.read_to += len(complete)
|
|
33
|
+
for line in complete.splitlines():
|
|
34
|
+
try:
|
|
35
|
+
record = json.loads(line)
|
|
36
|
+
except json.JSONDecodeError:
|
|
37
|
+
continue
|
|
38
|
+
if isinstance(record, dict):
|
|
39
|
+
self.add(record)
|
|
40
|
+
|
|
41
|
+
def add(self, record: dict[str, object]) -> None:
|
|
42
|
+
phase, key = record.get("phase"), record.get("id")
|
|
43
|
+
body = str(record.get("content", ""))
|
|
44
|
+
if phase == "delta" and key in self.streaming:
|
|
45
|
+
self.streaming[key].extend(body)
|
|
46
|
+
return
|
|
47
|
+
if phase == "stop":
|
|
48
|
+
if key in self.streaming:
|
|
49
|
+
self.streaming.pop(key).extend(body)
|
|
50
|
+
return
|
|
51
|
+
block = LogBlock(body)
|
|
52
|
+
if phase in STREAMING:
|
|
53
|
+
self.streaming[key] = block
|
|
54
|
+
self.mount(block)
|
|
55
|
+
self.display = True
|