| name | notebook |
| description | Produce a live rendered artifact (a chart, a computation, a small notebook) that flows inline into your chat answer — when, and only when, running code or plotting would genuinely serve the answer. You decide; writing the file is the only signal. |
Notebook — a live artifact in your answer
You can make a rendered output flow inline into the conversation — a chart, a computed table, a simulation, a small notebook — instead of only describing it in words. It appears right below your text answer.
When — you decide, entirely
Produce one only when running code, computing, or plotting genuinely serves the answer: a calculation worth showing its work, a data visualization, a simulation, a plot the person would actually want to see. For an ordinary conversational, explanatory, or textual answer, just answer in words — do not produce an artifact.
There is no rule forcing one and nothing inspects your answer to decide. Your judgment is the entire trigger: if you write a file, it embeds; if you don't, the answer stays clean text. Most turns won't warrant one. Bias toward clean text; reach for an artifact when it earns its place.
How
Write a self-contained index.html — the rendered output — into the directory named by the environment variable ANU_CHAT_ARTIFACT_DIR. It is served and embedded inline automatically; you never serve it, link it, or mention the mechanism. Always still answer in words alongside it, at whatever length the question deserves.
Minimal matplotlib example (works today):
import os, io, base64
import matplotlib; matplotlib.use("Agg")
import matplotlib.pyplot as plt
buf = io.BytesIO(); plt.savefig(buf, format="png", bbox_inches="tight")
img = base64.b64encode(buf.getvalue()).decode()
html = ('<!doctype html><meta charset=utf-8>'
'<body style="margin:1rem;font:14px ui-monospace,monospace;color:#000;background:#fff">'
f'<img style="max-width:100%" src="data:image/png;base64,{img}"></body>')
open(os.path.join(os.environ["ANU_CHAT_ARTIFACT_DIR"], "index.html"), "w").write(html)
A pure computation can render its result table/value as that HTML instead of a plot — same idea.
Live + interactive — a marimo notebook
When the output is worth interacting with — a slider to drag, a control to toggle, a parameter to sweep — write a marimo notebook instead of static HTML: a single *.py file in ANU_CHAT_ARTIFACT_DIR whose first cell does import marimo. You only write the file; the engine runs it in an isolated box and embeds the live app inline — you never run it, serve it, pick a port, or mention the mechanism. numpy, matplotlib, and marimo are already installed.
Same judgment as everything above: reach for this only when interactivity genuinely earns its place (there is a parameter the person would actually want to move). Otherwise a static figure — or plain text — is the better answer.
Minimal marimo notebook (a slider that redraws a plot live):
import marimo
app = marimo.App()
@app.cell
def _():
import marimo as mo
import numpy as np
import matplotlib.pyplot as plt
return mo, np, plt
@app.cell
def _(mo):
freq = mo.ui.slider(1, 10, value=3, label="frequency")
freq
return (freq,)
@app.cell
def _(freq, np, plt):
x = np.linspace(0, 2 * np.pi, 400)
fig, ax = plt.subplots(figsize=(6, 3))
ax.plot(x, np.sin(freq.value * x))
ax.set_title(f"sin({freq.value}·x)")
fig
return
if __name__ == "__main__":
app.run()
Write either a static index.html or one marimo *.py — not both. This skill evolves; the principle does not: you own the WHEN; the produced file is the only gate.
Electromagnetics / photonics — Tidy3D (in-box, no cloud)
tidy3d is installed in the notebook box and its local mode solver runs in-box — instant, free, no account. Reach for it when an EM/photonics structure or waveguide mode is worth showing interactively (e.g. a silicon waveguide with a width slider that replots the cross-section and the fundamental mode). For anisotropic materials (e.g. TFLN / lithium niobate) use the material library — td.material_library["LiNbO3"]["Zelmon1997"] — or an explicit td.AnisotropicMedium. Default to this local path for anything draggable.
Full FDTD (cloud) — only when explicitly asked. A real simulation (transmission spectrum, field propagation along the device — e.g. a directional coupler's through/cross power vs length) needs tidy3d.web.run(sim) on Flexcompute's cloud: it takes minutes and costs credits. Use it ONLY when the user clearly asks for a full/real simulation or a spectrum — and gate it behind a mo.ui.run_button() so it fires once on click, NEVER reactively / never on a slider move. Keep the local mode solver for the interactive parts. The host injects the key (mounted ~/.tidy3d); if none is configured, cloud calls error — that's expected, fall back to the local solver.
import tidy3d as td
from tidy3d.plugins.mode import ModeSolver
wl = 1.55; f0 = td.C_0 / wl
sim = td.Simulation(
size=(2.0, 1.5, 0.1), center=(0, 0, 0),
grid_spec=td.GridSpec.auto(wavelength=wl, min_steps_per_wvl=12),
structures=[td.Structure(
geometry=td.Box(center=(0, 0, 0), size=(width, 0.22, td.inf)),
medium=td.Medium(permittivity=3.47**2))],
medium=td.Medium(permittivity=1.44**2),
run_time=1e-12,
boundary_spec=td.BoundarySpec.all_sides(td.PML()),
)
ms = ModeSolver(simulation=sim, plane=td.Box(center=(0, 0, 0), size=(2.0, 1.5, 0)),
mode_spec=td.ModeSpec(num_modes=1), freqs=[f0])
data = ms.solve()
sim.plot(z=0, ax=ax)