
Notebooklm Studio
- 43 installs
- 105 repo stars
- Updated May 17, 2026
- toolsai/notebooklm-studio-skill
Helps with productivity & planning tasks.
About
notebooklm-studio is a Claude Code skill for productivity & planning. It helps solo builders move faster with AI-assisted development.
- notebooklm-studio
- Productivity & Planning
- AI-coding skill
Notebooklm Studio by the numbers
- 43 all-time installs (skills.sh)
- Ranked #1,700 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/toolsai/notebooklm-studio-skill --skill notebooklm-studioAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 43 |
|---|---|
| repo stars | ★ 105 |
| Last updated | May 17, 2026 |
| Repository | toolsai/notebooklm-studio-skill ↗ |
What it does
Helps with productivity & planning tasks.
Files
NotebookLM Studio
Overview
Use this skill when the user wants Codex to turn files, URLs, YouTube links, notes, transcripts, Drive-style sources, or research material into NotebookLM notebooks and NotebookLM Studio outputs. The goal is to make Codex act as the operator: prepare sources, create or reuse notebooks, generate official artifacts where available, download outputs, and explain limitations clearly.
This skill relies on the notebooklm CLI from notebooklm-py when available. That CLI is unofficial and uses undocumented NotebookLM endpoints, so every workflow must probe the environment before assuming it can run.
Operating Rules
1. Only ingest sources the user owns, provided, or is allowed to use. Do not bypass paywalls, DRM, private access controls, or copyright restrictions. 2. Prefer official NotebookLM outputs over local reconstructions. If an output is not exposed by the CLI or account plan, create a source-grounded fallback and label it as derived. 3. Keep artifacts traceable: return notebook title/id when known, source count, generated artifact names, local download paths, and any failed/skipped outputs. 4. For current feature availability, limits, and export formats, load references/notebooklm-capabilities.md. 5. For exact CLI commands and supported automation flags, load references/notebooklm-py-cli.md. 6. For mixed source handling and "any source" requests, load references/source-ingestion.md. 7. For output mapping and fallback decisions, load references/artifact-strategy.md.
First-Time Initialization
When a new user asks to initialize, set up, or start using this skill, make the setup as automatic as possible. The user should not be asked to manually configure package paths, folders, profiles, or commands unless an error requires it.
1. Run the bootstrap check:
python scripts/bootstrap_notebooklm.py --json --print-guide2. If notebooklm is missing, ask for permission to install dependencies, then run:
python scripts/bootstrap_notebooklm.py --install --print-guide3. If authentication is missing or expired, ask the user to complete the Google login step, then run:
python scripts/bootstrap_notebooklm.py --login --auth-test --print-guideThe login browser is the manual boundary: Codex can start it, but the user must choose the Google account, pass any MFA, and grant access. After login returns, Codex should run the environment check again and tell the user whether NotebookLM is ready.
4. Immediately after initialization, respond with a concise but complete guide. Prefer the generated guide from bootstrap_notebooklm.py --print-guide; load references/quickstart-guide.md if a fuller tutorial is useful.
5. If the user wants the local control room UI, start the dashboard from the current project/workspace:
python scripts/dashboard_server.py --host 127.0.0.1 --port 8765 --profile default --out-dir ./notebooklm_outputs/dashboardThen give the user the local URL. If port 8765 is occupied, choose the next available local port and report the actual URL.
Standard Workflow
1. Clarify the deliverable only if needed: audience, language, artifact types, output folder, and whether to create a new notebook or reuse an existing one. 2. Run an environment check:
python scripts/validate_environment.py --jsonIf notebooklm is missing, explain that NotebookLM automation needs notebooklm-py plus Google authentication. Ask before installing packages or opening browser login.
3. Build a source manifest before uploading:
python scripts/source_manifest.py --output /tmp/notebooklm-sources.json SOURCE...Review warnings for unsupported extensions, over-large files, too many sources, missing files, paywalled URLs, or YouTube videos without usable captions.
4. Create a dry-run plan with the optimized artifact pipeline:
python scripts/artifact_pipeline.py plan --notebook-title "Project Title" --source SOURCE --artifact audio --artifact video --artifact mind-map --artifact slide-deck --download --out-dir ./outputs5. Run only after the plan looks correct and authentication is ready:
python scripts/artifact_pipeline.py run --notebook-title "Project Title" --source SOURCE --artifact audio --artifact video --artifact mind-map --artifact slide-deck --download --out-dir ./outputs --status-file ./outputs/notebooklm-jobs.json6. Verify outputs: list artifacts, check local files exist, inspect file sizes, and summarize what was generated.
Optimized Artifact Pipeline
For multi-artifact requests, prefer scripts/artifact_pipeline.py over the older linear orchestrator. It avoids the "audio must finish before video starts" problem by submitting requested artifacts with --no-wait where NotebookLM supports it, then polling a shared job table. Completed artifacts are downloaded immediately; failed artifacts do not block unrelated outputs.
Use this pattern for requests like "make audio, video, mind map, and slides":
python scripts/artifact_pipeline.py run \
--notebook-title "Research Pack" \
--source ./report.pdf \
--source https://example.com/article \
--artifact audio \
--artifact video \
--artifact mind-map \
--artifact slide-deck \
--download \
--download-slide-format both \
--convert-mind-map-html \
--status-file ./notebooklm_outputs/research-pack-jobs.json \
--out-dir ./notebooklm_outputsThe pipeline should report each artifact as planned, submitted, in_progress, completed, downloaded, failed, submit_failed, download_failed, or timeout. Do not let a failed video prevent audio, mind-map, or slide-deck delivery.
Local Dashboard
This skill includes a portable local dashboard under dashboard/, served by scripts/dashboard_server.py. Use it when the user asks for a visual NotebookLM Studio control room, wants to browse notebooks, launch common artifact workflows, inspect generation status, or hand completed outputs back to Codex.
Start it from the user's active project folder so outputs are stored with that project:
python scripts/dashboard_server.py \
--host 127.0.0.1 \
--port 8765 \
--profile default \
--out-dir ./notebooklm_outputs/dashboardDashboard behavior:
- Lists notebooks, sources, native NotebookLM artifacts, and recent generated jobs.
- Provides nine goal-based workflow recipes plus the nine NotebookLM native Studio tools.
- Submits real NotebookLM CLI jobs in the background and downloads completed artifacts.
- Writes handoff files and a plain-text
latest_agent_prompt.mdfor Codex analysis. - Uses manual prompt copy fallback: it opens a selected prompt panel and asks the user to press Command+C / Command+V when browser clipboard access is blocked.
Dashboard safety and portability:
- Bind to
127.0.0.1by default. Do not bind to0.0.0.0unless the user explicitly understands the network exposure. - Keep generated outputs under
--out-dir; the default is the current workspace'snotebooklm_outputs/dashboard. - Treat the dashboard as an operator UI for the user's authorized NotebookLM account, not as a public service.
Mind Map Visualization
NotebookLM downloads mind maps as JSON. When a user asks for a mind map, treat the JSON as the canonical official artifact, then automatically create an interactive local HTML view:
python scripts/mindmap_html.py ./notebooklm_outputs/mind-map.json -o ./notebooklm_outputs/mind-map.htmlartifact_pipeline.py does this automatically when --convert-mind-map-html is enabled, which is the default. Return both files: JSON for traceability, HTML for human use.
Artifact Defaults
Use these artifact names with scripts/nblm_orchestrator.py --artifact and the notebooklm generate CLI:
audio: Audio Overview / podcast-style summary.video: Video Overview.cinematic-video: cinematic Video Overview when the user has access.report: NotebookLM report; use--report-format briefing-doc,study-guide,blog-post, orcustomwhen appropriate.mind-map: mind map artifact.quiz: interactive quiz.flashcards: flashcard deck.slide-deck: NotebookLM slide deck.infographic: NotebookLM infographic.data-table: structured table export.
For requested outputs such as deep-analysis JSON, editorial scripts, translated briefs, or custom tables that are not direct Studio artifacts, use notebooklm ask --json or a generated report as the grounding layer, then create a derived local artifact with citations and mark it as derived.
User-Facing Result Contract
When the workflow finishes, answer with:
- What notebook was used or created.
- What sources were added and which were skipped.
- Which outputs are official NotebookLM artifacts and which are derived by Codex.
- Local artifact paths, using absolute paths when files exist.
- Any account-plan limits, age-gated features, authentication failures, or known NotebookLM inaccuracies that affected the result.
Do not claim a generated artifact exists until it has been downloaded or verified in NotebookLM.
.DS_Store
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.ruff_cache/
.env
.env.*
!.env.example
.venv/
venv/
env/
notebooklm_outputs/
outputs/
downloads/
tmp/
temp/
*.log
*.sqlite
*.sqlite3
*.db
*.token
*.secret
*.session
credentials.json
token.json
interface:
display_name: "NotebookLM Studio"
short_description: "Run NotebookLM artifacts and launch the local Studio dashboard from Codex."
default_prompt: "Use NotebookLM Studio to ingest authorized sources, create or reuse notebooks, generate official NotebookLM artifacts, download outputs, launch the local dashboard when useful, and report source-grounded results with limitations."
const state = {
notebooks: [],
overviews: new Map(),
loadingOverviews: new Set(),
currentNotebook: null,
filter: "all",
query: "",
jobs: [],
copiedPrompts: new Set(),
};
const recipes = [
{
id: "executive-briefing-suite",
name: "Executive Briefing Suite",
purpose: "Help leadership quickly grasp key findings, decision rationale, and presentation-ready materials.",
artifacts: ["Briefing Report", "Slide Deck", "Data Table", "Audio Overview"],
native: ["report", "slide-deck", "data-table", "audio"],
},
{
id: "visual-story-suite",
name: "Visual Story Suite",
purpose: "Turn complex material into a visual narrative for external presentations or internal alignment.",
artifacts: ["Video Overview", "Slide Deck", "Infographic", "Mind Map"],
native: ["video", "slide-deck", "infographic", "mind-map"],
},
{
id: "learning-masterpack",
name: "Learning Masterpack",
purpose: "Convert sources into study and review materials for courses, training, and research.",
artifacts: ["Study Guide Report", "Quiz", "Flashcards", "Mind Map", "Audio Overview"],
native: ["report", "quiz", "flashcards", "mind-map", "audio"],
},
{
id: "competitive-intel-suite",
name: "Competitive Intel Suite",
purpose: "Frame competitors, risks, differentiation, and market position with native NotebookLM artifacts.",
artifacts: ["Custom Report", "Data Table", "Slide Deck", "Infographic"],
native: ["report", "data-table", "slide-deck", "infographic"],
},
{
id: "due-diligence-suite",
name: "Due Diligence Review Suite",
purpose: "Surface red flags, open questions, evidence maps, and decision summaries from large source sets.",
artifacts: ["Custom Report", "Data Table", "Mind Map", "Quiz"],
native: ["report", "data-table", "mind-map", "quiz"],
},
{
id: "content-distribution-suite",
name: "Content Distribution Suite",
purpose: "Repurpose one source set into distribution-ready assets grounded in NotebookLM outputs.",
artifacts: ["Blog-post Report", "Video Overview", "Infographic", "Slide Deck"],
native: ["report", "video", "infographic", "slide-deck"],
},
{
id: "research-synthesis-suite",
name: "Research Synthesis Suite",
purpose: "Turn large source libraries into a grounded research brief, evidence map, and reusable extraction table.",
artifacts: ["Research Report", "Mind Map", "Data Table", "Audio Overview"],
native: ["report", "mind-map", "data-table", "audio"],
},
{
id: "stakeholder-qa-suite",
name: "Stakeholder Q&A Suite",
purpose: "Prepare for meetings, interviews, and reviews with crisp talking points plus recall checks.",
artifacts: ["Briefing Report", "Slide Deck", "Quiz", "Flashcards"],
native: ["report", "slide-deck", "quiz", "flashcards"],
},
{
id: "knowledge-base-digest-suite",
name: "Knowledge Base Digest Suite",
purpose: "Refresh a notebook into a compact digest, structured table, map, and listenable summary.",
artifacts: ["Digest Report", "Data Table", "Mind Map", "Audio Overview"],
native: ["report", "data-table", "mind-map", "audio"],
},
];
const studioActions = [
["audio", "Audio Overview", "Podcast-style summary"],
["slide-deck", "Slide Deck", "PDF + PPTX presentation"],
["video", "Video Overview", "Narrated visual explainer"],
["mind-map", "Mind Map", "Interactive concept graph"],
["report", "Report", "Briefing / study guide / blog post"],
["flashcards", "Flashcards", "Study card deck"],
["quiz", "Quiz", "Interactive assessment"],
["infographic", "Infographic", "Single visual summary"],
["data-table", "Data Table", "Structured CSV extraction"],
];
const PREVIEW_NOTE = "Preview only - not submitted to NotebookLM";
const REAL_NOTE = "Submitted through NotebookLM CLI";
const artifactLabels = {
audio: "Audio",
video: "Video",
report: "Report",
slides: "Slides",
"slide-deck": "Slides",
"mind-map": "Mind Map",
table: "Table",
"data-table": "Table",
quiz: "Quiz",
flashcards: "Cards",
infographic: "Info",
};
const els = {};
document.addEventListener("DOMContentLoaded", async () => {
bindElements();
bindEvents();
installWheelScrollFallback();
renderRecipes();
renderStudioActions();
await loadNotebooks();
await pollJobs();
setInterval(pollJobs, 5000);
});
function bindElements() {
[
"libraryView",
"detailView",
"notebookGrid",
"librarySearch",
"filterPills",
"totalNotebooks",
"readyCount",
"needsReviewCount",
"backButton",
"homeButton",
"detailTitle",
"detailMeta",
"sourceList",
"sourceCountBadge",
"artifactCountBadge",
"recipeList",
"studioGrid",
"timeline",
"commandButton",
"commandOverlay",
"commandInput",
"commandResults",
"clearJobsButton",
"recipeHelpButton",
"runnerStatus",
].forEach((id) => (els[id] = document.getElementById(id)));
}
function bindEvents() {
els.librarySearch.addEventListener("input", (event) => {
state.query = event.target.value.trim().toLowerCase();
renderNotebooks();
hydrateVisibleOverviews();
});
els.filterPills.addEventListener("click", (event) => {
const button = event.target.closest("button");
if (!button) return;
state.filter = button.dataset.filter;
[...els.filterPills.querySelectorAll("button")].forEach((item) => item.classList.toggle("active", item === button));
renderNotebooks();
hydrateVisibleOverviews();
});
els.backButton.addEventListener("click", showLibrary);
els.homeButton.addEventListener("click", showLibrary);
els.commandButton.addEventListener("click", openCommand);
els.commandOverlay.addEventListener("click", (event) => {
if (event.target === els.commandOverlay) closeCommand();
});
els.commandInput.addEventListener("input", renderCommandResults);
els.clearJobsButton.addEventListener("click", async () => {
try {
const result = await fetchJson("/api/jobs", { method: "DELETE" });
state.jobs = (result.jobs || []).map(normalizeJob);
} catch {
state.jobs = [];
}
renderTimeline();
updateRunnerStatus();
});
els.recipeHelpButton.addEventListener("click", () => {
addLocalJob({
label: "Background Runner onboarding",
kind: "guide",
artifacts: ["Start server", "Connect CLI", "Submit real jobs"],
progress: 100,
status: "ready",
steps: [
{ name: "Prompt: Launch NotebookLM Dashboard", status: "done" },
{ name: "Server: python3 server.py --port 8765", status: "done" },
{ name: "Buttons: submit NotebookLM generate commands", status: "done" },
],
});
});
document.addEventListener("keydown", (event) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
event.preventDefault();
openCommand();
}
if (event.key === "Escape") closePromptPanel();
if (event.key === "Escape") closeCommand();
});
document.querySelectorAll("[data-theme-choice]").forEach((button) => {
button.addEventListener("click", () => {
document.body.dataset.theme = button.dataset.themeChoice;
document.querySelectorAll("[data-theme-choice]").forEach((item) => item.classList.toggle("active", item === button));
});
});
}
function installWheelScrollFallback() {
window.addEventListener("wheel", (event) => {
if (event.defaultPrevented || (!event.deltaY && !event.deltaX)) return;
const target = event.target instanceof Element ? event.target : null;
const root = pageScroller();
const scrollConsumer = findScrollableConsumer(target, event.deltaX, event.deltaY);
if (scrollConsumer && scrollConsumer !== root) return;
const beforeTop = root.scrollTop;
const beforeLeft = root.scrollLeft;
requestAnimationFrame(() => {
if (root.scrollTop !== beforeTop || root.scrollLeft !== beforeLeft) return;
try {
root.scrollTop = beforeTop + event.deltaY;
root.scrollLeft = beforeLeft + event.deltaX;
} catch {
// Some embedded browsers expose read-only document scroll APIs; native wheel scrolling still handles the app shell.
}
});
}, { passive: true });
}
function pageScroller() {
return document.querySelector(".app-shell") || document.scrollingElement || document.documentElement;
}
function findScrollableConsumer(target, deltaX, deltaY) {
for (let node = target; node && node !== document.body; node = node.parentElement) {
const style = getComputedStyle(node);
const canScrollY = /(auto|scroll|overlay)/.test(style.overflowY) && node.scrollHeight > node.clientHeight;
const canScrollX = /(auto|scroll|overlay)/.test(style.overflowX) && node.scrollWidth > node.clientWidth;
const hasYRoom = deltaY < 0 ? node.scrollTop > 0 : node.scrollTop + node.clientHeight < node.scrollHeight;
const hasXRoom = deltaX < 0 ? node.scrollLeft > 0 : node.scrollLeft + node.clientWidth < node.scrollWidth;
if ((canScrollY && hasYRoom) || (canScrollX && hasXRoom)) return node;
}
return null;
}
async function loadNotebooks() {
const data = await fetchJson("/api/notebooks");
state.notebooks = data.notebooks || [];
renderNotebooks();
hydrateVisibleOverviews();
}
async function hydrateVisibleOverviews() {
const visible = filteredNotebooks().slice(0, 12);
for (const notebook of visible) {
fetchOverview(notebook);
await sleep(120);
}
}
async function fetchOverview(notebook) {
if (!notebook?.id || state.overviews.has(notebook.id) || state.loadingOverviews.has(notebook.id)) return null;
state.loadingOverviews.add(notebook.id);
try {
const overview = await fetchJson(`/api/notebook/${notebook.id}/overview`);
state.overviews.set(notebook.id, overview);
renderNotebooks();
updateStats();
if (state.currentNotebook?.id === notebook.id) renderDetail();
return overview;
} catch {
return null;
} finally {
state.loadingOverviews.delete(notebook.id);
}
}
function filteredNotebooks() {
return state.notebooks.filter((notebook) => {
const overview = state.overviews.get(notebook.id);
const haystack = `${notebook.display_title} ${notebook.id} ${notebook.created_at}`.toLowerCase();
if (state.query && !haystack.includes(state.query)) return false;
if (state.filter === "empty") return notebook.warnings?.empty_title;
if (state.filter === "warning") return notebook.warnings?.empty_title || notebook.warnings?.possible_duplicate || overview?.failed_count;
if (state.filter === "ready") return overview?.status === "Ready";
return true;
});
}
function renderNotebooks() {
const template = document.getElementById("notebookCardTemplate");
els.notebookGrid.innerHTML = "";
for (const notebook of filteredNotebooks()) {
const overview = state.overviews.get(notebook.id);
const card = template.content.firstElementChild.cloneNode(true);
const status = overview?.status || (notebook.warnings?.empty_title ? "Needs review" : "Scanning");
const activityAt = overview?.last_activity || notebook.created_at;
card.querySelector(".status-badge").textContent = status;
card.querySelector(".status-badge").classList.toggle("status-ready", status === "Ready");
card.querySelector(".status-badge").classList.toggle("status-warning", status !== "Ready" && status !== "Has failed artifact");
card.querySelector(".status-badge").classList.toggle("status-failed", status === "Has failed artifact");
card.querySelector(".date-chip").textContent = `Synced ${formatDate(notebook.created_at)}`;
card.querySelector("h3").textContent = notebook.display_title;
card.querySelector(".id-line").textContent = notebook.id;
card.querySelector('[data-field="source-count"]').textContent = overview?.source_count ?? "…";
card.querySelector('[data-field="artifact-count"]').textContent = overview?.artifact_count ?? "…";
card.querySelector('[data-field="last-activity"]').textContent = activityAt ? formatDate(activityAt) : "sync";
const strip = card.querySelector(".artifact-strip");
const kinds = overview?.completed_kinds?.length ? overview.completed_kinds : ["audio", "video", "report", "slides", "table"].slice(0, 3);
kinds.forEach((kind) => {
const pill = document.createElement("span");
pill.className = `artifact-pill ${overview?.completed_kinds?.includes(kind) ? "done" : ""}`;
pill.textContent = artifactLabels[kind] || kind;
strip.appendChild(pill);
});
card.addEventListener("click", () => openNotebook(notebook));
els.notebookGrid.appendChild(card);
}
updateStats();
}
function updateStats() {
const ready = [...state.overviews.values()].filter((overview) => overview.status === "Ready").length;
const review = state.notebooks.filter((notebook) => notebook.warnings?.empty_title).length
+ [...state.overviews.values()].filter((overview) => overview.failed_count > 0).length;
els.totalNotebooks.textContent = state.notebooks.length;
els.readyCount.textContent = ready || "—";
els.needsReviewCount.textContent = review;
}
async function openNotebook(notebook) {
state.currentNotebook = notebook;
els.libraryView.classList.remove("active");
els.detailView.classList.add("active");
renderDetail();
if (!state.overviews.has(notebook.id)) {
await fetchOverview(notebook);
}
}
function showLibrary() {
els.detailView.classList.remove("active");
els.libraryView.classList.add("active");
state.currentNotebook = null;
}
function renderDetail() {
const notebook = state.currentNotebook;
if (!notebook) return;
const overview = state.overviews.get(notebook.id);
els.detailTitle.textContent = notebook.display_title;
const activityAt = overview?.last_activity || notebook.created_at;
els.detailMeta.textContent = `${notebook.id} · Latest activity ${formatDate(activityAt)} · ${overview?.status || "Scanning"}`;
renderSources(overview?.sources, Boolean(overview));
renderArtifacts(overview?.artifacts || []);
renderTimeline();
}
function renderSources(sources, hasSynced = true) {
const sourceItems = Array.isArray(sources) ? sources : [];
els.sourceCountBadge.textContent = hasSynced ? (sourceItems.length || "—") : "sync";
els.sourceList.innerHTML = "";
if (!hasSynced) {
els.sourceList.innerHTML = `<div class="source-item"><strong>Syncing sources...</strong></div>`;
return;
}
if (!sourceItems.length) {
els.sourceList.innerHTML = `<div class="source-item"><strong>No sources found</strong></div>`;
return;
}
sourceItems.forEach((source) => {
const item = document.createElement("div");
item.className = "source-item";
item.innerHTML = `
<strong>${escapeHtml(displaySourceTitle(source))}</strong>
`;
els.sourceList.appendChild(item);
});
}
function displaySourceTitle(source) {
const title = String(source?.title || "").trim();
if (!title) return "Untitled source";
if (title === "\u8cbc\u4e0a\u7684\u6587\u5b57") return "Pasted text";
return title;
}
function displaySourceType(value) {
const raw = String(value || "Source").trim();
const known = {
"SourceType.PASTED_TEXT": "Pasted text",
PASTED_TEXT: "Pasted text",
"SourceType.URL": "Web source",
URL: "Web source",
"SourceType.PDF": "PDF",
PDF: "PDF",
"SourceType.DOC": "Document",
"SourceType.DOCX": "Document",
DOC: "Document",
DOCX: "Document",
};
if (known[raw]) return known[raw];
return raw
.replace(/^SourceType\./, "")
.replace(/_/g, " ")
.toLowerCase()
.replace(/\b\w/g, (char) => char.toUpperCase());
}
function formatStatus(value) {
return String(value || "unknown")
.replace(/_/g, " ")
.toLowerCase()
.replace(/\b\w/g, (char) => char.toUpperCase());
}
function renderRecipes() {
els.recipeList.innerHTML = "";
recipes.forEach((recipe) => {
const card = document.createElement("article");
card.className = "recipe-card";
card.innerHTML = `
<h4>${escapeHtml(recipe.name)}</h4>
<p>${escapeHtml(recipe.purpose)}</p>
<div class="output-chips">${recipe.artifacts.map((item) => `<span>${escapeHtml(item)}</span>`).join("")}</div>
<button class="run-button" type="button" aria-label="Start ${escapeHtml(recipe.name)}">Start</button>
`;
card.querySelector("button").addEventListener("click", () => startJob(recipe.name, "recipe", recipe.artifacts, recipe.native, recipe.purpose));
els.recipeList.appendChild(card);
});
}
function renderStudioActions() {
els.studioGrid.innerHTML = "";
studioActions.forEach(([id, label, description]) => {
const button = document.createElement("button");
button.className = "studio-button";
button.type = "button";
button.dataset.toolId = id;
button.innerHTML = `
<span class="studio-icon">${studioGlyph(id)}</span>
<span class="studio-copy"><h4>${label}</h4><span class="studio-description">${description}</span></span>
<span class="tool-arrow" aria-hidden="true">›</span>
`;
button.addEventListener("click", () => startJob(label, "native", [label], [id], description));
els.studioGrid.appendChild(button);
});
}
function renderArtifacts(artifacts) {
els.artifactCountBadge.textContent = studioActions.length;
}
async function startJob(label, kind, artifacts, commands = [], purpose = "") {
if (!state.currentNotebook) return;
try {
const result = await fetchJson("/api/jobs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
notebook_id: state.currentNotebook.id,
notebook_title: state.currentNotebook.display_title,
label,
kind,
purpose,
artifacts,
commands,
}),
});
if (result.job) addLocalJob(result.job);
} catch {
addLocalJob({ label, kind, artifacts, status: "queued", progress: 18, preview: true });
}
}
function addLocalJob(job) {
const normalized = normalizeJob(job);
upsertJob(normalized);
renderTimeline();
updateRunnerStatus(normalized);
document.querySelector(".timeline-panel")?.scrollIntoView({ behavior: "smooth", block: "start" });
pollJobs();
}
function normalizeJob(job) {
return {
id: job.id || `local-${Date.now()}`,
notebook_id: job.notebook_id,
label: job.label,
kind: job.kind || "task",
purpose: job.purpose,
artifacts: job.artifacts || [],
commands: job.commands || [],
outputs: job.outputs || [],
status: job.status || "queued",
progress: job.progress ?? 3,
created_at: job.created_at || new Date().toISOString().slice(0, 19),
updated_at: job.updated_at,
steps: job.steps || [{ name: "Queued for NotebookLM", status: "active" }],
preview: Boolean(job.preview),
output_dir: job.output_dir,
handoff_path: job.handoff_path,
global_handoff_path: job.global_handoff_path,
prompt_path: job.prompt_path || job.agent_prompt_path,
handoff_status: job.handoff_status,
prompt_copied: Boolean(job.prompt_copied) || state.copiedPrompts.has(job.id),
prompt_error: job.prompt_error,
};
}
function upsertJob(job) {
const index = state.jobs.findIndex((item) => item.id === job.id);
if (index >= 0) state.jobs.splice(index, 1, job);
else state.jobs.unshift(job);
}
async function pollJobs() {
try {
const result = await fetchJson("/api/jobs");
state.jobs = (result.jobs || []).map(normalizeJob);
renderTimeline();
updateRunnerStatus();
} catch {
// Keep the last visible state if the local runner is temporarily busy.
}
}
function updateRunnerStatus(job = null) {
const currentJobs = state.currentNotebook
? state.jobs.filter((item) => item.notebook_id === state.currentNotebook.id)
: state.jobs;
const latest = job || currentJobs[0];
if (!latest) {
els.runnerStatus.textContent = "Ready · submits jobs through NotebookLM CLI";
return;
}
els.runnerStatus.textContent = `${latest.label} · ${latest.status} · ${latest.preview ? PREVIEW_NOTE : REAL_NOTE}`;
}
async function copyPromptToAgent(jobId) {
let result;
try {
result = await fetchJson("/api/handoff", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ job_id: jobId }),
});
} catch {
const job = state.jobs.find((item) => item.id === jobId);
if (job) {
job.prompt_error = "Prompt request failed";
renderTimeline();
}
return;
}
const job = state.jobs.find((item) => item.id === jobId);
if (!job) return;
job.handoff_path = result.path;
job.global_handoff_path = result.global_path;
job.prompt_path = result.prompt_path;
if (result.ok) {
showPromptPanel(result.prompt, result.prompt_path);
job.handoff_status = "prompt_ready";
job.prompt_error = "Prompt is selected in the popup. Press Command+C, then paste with Command+V.";
updateRunnerStatus(job);
renderTimeline();
} else {
job.prompt_error = result.error || "Prompt is not ready yet";
renderTimeline();
}
}
function showPromptPanel(prompt, promptPath) {
let overlay = document.getElementById("promptOverlay");
if (!overlay) {
overlay = document.createElement("div");
overlay.id = "promptOverlay";
overlay.className = "prompt-overlay";
overlay.innerHTML = `
<section class="prompt-panel" role="dialog" aria-modal="true" aria-labelledby="promptPanelTitle">
<div class="prompt-panel-head">
<div>
<p class="section-label">Agent Handoff</p>
<h3 id="promptPanelTitle">Prompt ready</h3>
</div>
<button class="tiny-button" type="button" data-prompt-close>Close</button>
</div>
<p class="prompt-status" data-prompt-status></p>
<textarea class="prompt-textarea" spellcheck="false" readonly data-prompt-text></textarea>
<div class="prompt-footer">
<small data-prompt-path></small>
<div>
<button class="tiny-button" type="button" data-prompt-select>Select all</button>
<button class="tiny-button" type="button" data-prompt-close>Close</button>
</div>
</div>
</section>
`;
overlay.addEventListener("click", (event) => {
if (event.target === overlay || event.target.closest("[data-prompt-close]")) closePromptPanel();
});
overlay.querySelector("[data-prompt-select]").addEventListener("click", () => selectPromptText());
document.body.appendChild(overlay);
}
overlay.querySelector("[data-prompt-text]").value = String(prompt ?? "");
overlay.querySelector("[data-prompt-path]").textContent = promptPath || "";
overlay.querySelector("[data-prompt-status]").textContent = "The prompt is selected. Press Command+C to copy, then paste it into Codex with Command+V.";
overlay.classList.add("active");
overlay.setAttribute("aria-hidden", "false");
selectPromptText();
}
function selectPromptText() {
const textarea = document.querySelector("#promptOverlay [data-prompt-text]");
if (!textarea) return;
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(0, textarea.value.length);
textarea.scrollTop = 0;
textarea.scrollLeft = 0;
}, 20);
}
function closePromptPanel() {
const overlay = document.getElementById("promptOverlay");
if (!overlay) return;
overlay.classList.remove("active");
overlay.setAttribute("aria-hidden", "true");
}
function renderTimeline() {
els.timeline.innerHTML = "";
const overview = state.currentNotebook ? state.overviews.get(state.currentNotebook.id) : null;
const artifactJobs = (overview?.artifacts || []).slice(0, 6).map((artifact) => ({
id: artifact.id,
label: artifact.title || artifact.type,
kind: "native artifact",
artifacts: [artifact.type],
status: artifact.status,
progress: artifact.status === "completed" ? 100 : artifact.status === "failed" ? 100 : 45,
created_at: artifact.created_at,
steps: [
{ name: "Submitted", status: "done" },
{ name: artifact.status === "failed" ? "Failed in NotebookLM" : "Generated", status: artifact.status === "failed" ? "failed" : "done" },
{ name: "Download available", status: artifact.status === "completed" ? "done" : "waiting" },
],
}));
const activeJobs = state.currentNotebook
? state.jobs.filter((job) => job.notebook_id === state.currentNotebook.id || job.kind === "guide")
: state.jobs;
const jobs = [...activeJobs, ...artifactJobs];
if (!jobs.length) {
const emptyTitle = state.currentNotebook && !overview ? "Syncing artifact history..." : "No jobs yet";
const emptyBody = state.currentNotebook && !overview
? "Existing NotebookLM artifacts will appear here after metadata sync."
: "Start a recipe or native Studio action to see live progress here.";
els.timeline.innerHTML = `<div class="job-card"><div><strong>${emptyTitle}</strong><small>${emptyBody}</small></div></div>`;
return;
}
jobs.forEach((job) => {
const outputs = job.outputs || [];
const hasDownloads = (job.outputs || []).some((output) => output.downloaded_files?.length);
const isRunnerJob = !job.preview && (job.commands?.length || job.outputs?.length || job.output_dir);
const isExternalArtifact = job.kind === "native artifact" && !outputs.length;
const isSingleOutputJob = !isExternalArtifact && outputs.length === 1;
const canCopyPrompt = isRunnerJob && ["completed", "completed with failures"].includes(job.status) && hasDownloads;
const actionHint = canCopyPrompt
? "Prompt opens selected and ready for Command+C."
: "Available after downloads finish.";
const promptDetail = job.prompt_error || actionHint;
const promptTitle = job.prompt_path || job.global_handoff_path || promptDetail;
const outputsHtml = job.outputs?.length ? `
<div class="output-list">${job.outputs.map((output) => `
<span>
<strong>${escapeHtml(output.label || output.command)}</strong>
<em>${escapeHtml(output.status || "queued")}${output.artifact_id ? ` · ${escapeHtml(output.artifact_id)}` : ""}${output.download_status ? ` · ${escapeHtml(output.download_status)}` : ""}</em>
</span>
`).join("")}</div>
` : "";
const stepsHtml = (job.steps || []).map((step) => `<span>${step.status === "done" ? "✓" : step.status === "failed" ? "!" : "·"} ${escapeHtml(step.name)}</span>`).join("");
const card = document.createElement("div");
card.className = `job-card${isExternalArtifact ? " job-card-summary" : ""}${isSingleOutputJob ? " job-card-single" : ""}`;
if (isExternalArtifact) {
const summaryStep = (job.steps || []).find((step) => step.status === "failed")
|| (job.steps || []).find((step) => step.name === "Download available")
|| (job.steps || [])[0];
const summaryMark = summaryStep?.status === "failed" ? "!" : summaryStep?.status === "done" ? "✓" : "·";
card.innerHTML = `
<div class="job-summary-row">
<div class="job-title-block">
<strong>${escapeHtml(job.label || "NotebookLM artifact")}</strong>
<small>${escapeHtml(job.kind || "native artifact")} · ${escapeHtml(job.status || "queued")} · ${formatDate(job.created_at)}</small>
</div>
<div class="job-summary-status">
<span>${summaryMark} ${escapeHtml(summaryStep?.name || formatStatus(job.status))}</span>
<em>${(job.artifacts || []).map((item) => escapeHtml(item)).join(" · ")}</em>
</div>
</div>
<div class="progress-track"><i style="width:${job.progress || 0}%"></i></div>
`;
} else {
card.innerHTML = `
<div class="job-header-row">
<div class="job-title-block">
<strong>${escapeHtml(job.label || "NotebookLM job")}</strong>
<small>${escapeHtml(job.kind || "task")} · ${escapeHtml(job.status || "queued")} · ${formatDate(job.created_at)}</small>
</div>
${isRunnerJob ? `<div class="job-action-panel"><button type="button" class="handoff-button" data-job-id="${escapeHtml(job.id)}" ${canCopyPrompt ? "" : "disabled"}>Show Prompt for Codex</button><small title="${escapeHtml(promptTitle)}">${escapeHtml(promptDetail)}</small></div>` : ""}
</div>
<div class="job-body">
<div class="output-chips">${(job.artifacts || []).map((item) => `<span>${escapeHtml(item)}</span>`).join("")}</div>
${job.preview ? `<div class="preview-warning">${PREVIEW_NOTE}</div>` : ""}
<div class="job-status-grid">
${outputsHtml || "<div></div>"}
<div class="step-list">
${stepsHtml}
</div>
</div>
<div class="progress-track"><i style="width:${job.progress || 0}%"></i></div>
</div>
`;
}
card.querySelector(".handoff-button")?.addEventListener("click", (event) => {
event.stopPropagation();
if (!event.currentTarget.disabled) copyPromptToAgent(job.id);
});
els.timeline.appendChild(card);
});
}
function openCommand() {
els.commandOverlay.classList.add("active");
els.commandOverlay.setAttribute("aria-hidden", "false");
els.commandInput.value = "";
renderCommandResults();
setTimeout(() => els.commandInput.focus(), 20);
}
function closeCommand() {
els.commandOverlay.classList.remove("active");
els.commandOverlay.setAttribute("aria-hidden", "true");
}
function renderCommandResults() {
const query = els.commandInput.value.trim().toLowerCase();
const artifactQuery = {
audio: "audio",
video: "video",
report: "report",
slides: "slides",
slide: "slides",
table: "table",
data: "table",
mindmap: "mind-map",
"mind map": "mind-map",
infographic: "infographic",
quiz: "quiz",
flashcards: "flashcards",
}[query];
const results = state.notebooks.filter((notebook) => {
if (!query) return true;
if (query === "untitled") return notebook.warnings?.empty_title;
if (/^\d{4}-\d{2}$/.test(query)) return String(notebook.created_at || "").startsWith(query);
if (artifactQuery) return state.overviews.get(notebook.id)?.completed_kinds?.includes(artifactQuery);
return `${notebook.display_title} ${notebook.id}`.toLowerCase().includes(query);
}).slice(0, 10);
els.commandResults.innerHTML = "";
results.forEach((notebook) => {
const row = document.createElement("button");
row.className = "command-result";
row.type = "button";
row.innerHTML = `
<span><strong>${escapeHtml(notebook.display_title)}</strong><small>${escapeHtml(notebook.id)} · ${formatDate(notebook.created_at)}</small></span>
<span class="icon-arrow"></span>
`;
row.addEventListener("click", () => {
closeCommand();
openNotebook(notebook);
});
els.commandResults.appendChild(row);
});
}
async function fetchJson(url, options) {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
return response.json();
}
function formatDate(value) {
if (!value) return "—";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value).slice(0, 10);
return new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }).format(date);
}
function studioGlyph(id) {
const icons = {
audio: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 13v-2m4 6V7m4 13V4m4 12V8m4 5v-2"/><path d="M17 4l.8 1.8L20 6.5l-1.8.8L17.5 9l-.8-1.8L15 6.5l1.8-.7L17 4Z"/></svg>`,
video: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 7h12v10H5z"/><path d="M9 4h7M8 20h8M10 10l4 2-4 2z"/></svg>`,
report: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 4h9l3 3v13H7z"/><path d="M16 4v4h4M10 11h6M10 15h6M10 18h4"/></svg>`,
"slide-deck": `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16v11H4z"/><path d="M8 20h8M12 17v3"/></svg>`,
"mind-map": `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 6v5M7 17l5-6 5 6"/><path d="M9 4h6v4H9zM4 15h6v5H4zM14 15h6v5h-6z"/></svg>`,
"data-table": `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5h14v14H5zM5 10h14M5 15h14M10 5v14M15 5v14"/></svg>`,
quiz: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5h14v14H5z"/><path d="M9.5 10a2.5 2.5 0 1 1 4.2 1.8c-.8.7-1.2 1-1.2 2.2M12.5 17h.1"/></svg>`,
flashcards: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7h10v12H7z"/><path d="M4 4h10v12M10 11h4M10 15h3"/><path d="M16 4l.6 1.4L18 6l-1.4.6L16 8l-.6-1.4L14 6l1.4-.6L16 4Z"/></svg>`,
infographic: `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 19V9M12 19V5M19 19v-8"/><path d="M4 19h16M9 9h6M16 11h6M2 9h6"/></svg>`,
};
return icons[id] || `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5h14v14H5z"/></svg>`;
}
function escapeHtml(value) {
return String(value ?? "").replace(/[&<>"']/g, (char) => ({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
})[char]);
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>NotebookLM Studio Dashboard</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body data-theme="obsidian">
<div class="ambient ambient-one"></div>
<div class="ambient ambient-two"></div>
<div class="grain"></div>
<main class="app-shell">
<header class="topbar">
<button class="brand" id="homeButton" type="button" aria-label="Back to library">
<span class="brand-mark">
<img src="/assets/NotebookLM_logo.png" alt="" />
</span>
<span>
<strong>NotebookLM Studio</strong>
<small>Research control room</small>
</span>
</button>
<div class="top-actions">
<button class="ghost-button" id="commandButton" type="button">
<span class="icon-search"></span>
Command
<kbd>⌘K</kbd>
</button>
<div class="theme-switcher" aria-label="Design variants">
<button type="button" data-theme-choice="obsidian" class="active">Obsidian</button>
<button type="button" data-theme-choice="pearl">Pearl</button>
<button type="button" data-theme-choice="signal">Signal</button>
</div>
</div>
</header>
<section class="library-view view active" id="libraryView">
<div class="hero-panel">
<div>
<p class="section-label">Library Dashboard</p>
<h1>NotebookLM Studio by Prompt Case</h1>
</div>
<div class="hero-stats">
<div><strong id="totalNotebooks">—</strong><span>Notebooks</span></div>
<div><strong id="readyCount">—</strong><span>Ready</span></div>
<div><strong id="needsReviewCount">—</strong><span>Needs review</span></div>
</div>
</div>
<div class="library-toolbar">
<label class="search-box">
<span class="icon-search"></span>
<input id="librarySearch" placeholder="Search notebooks, IDs, topics..." />
</label>
<div class="filter-pills" id="filterPills">
<button class="active" data-filter="all">All</button>
<button data-filter="ready">Ready</button>
<button data-filter="warning">Warnings</button>
<button data-filter="empty">Untitled</button>
</div>
</div>
<div class="notebook-grid" id="notebookGrid"></div>
</section>
<section class="detail-view view" id="detailView">
<div class="detail-header">
<button class="back-button" id="backButton" type="button">
<span class="icon-arrow icon-arrow-left"></span>
Library
</button>
<div>
<p class="section-label">Notebook Data Room</p>
<h2 id="detailTitle">Notebook</h2>
<p id="detailMeta" class="muted">Loading notebook metadata...</p>
</div>
<div class="runner-card">
<span class="pulse-dot"></span>
<div>
<strong>Real Runner</strong>
<small id="runnerStatus">Ready · submits jobs through NotebookLM CLI</small>
</div>
</div>
</div>
<div class="data-room">
<aside class="sources-panel panel">
<div class="panel-head">
<div>
<p class="section-label">Left rail</p>
<h3>Sources</h3>
</div>
<span id="sourceCountBadge" class="count-badge">—</span>
</div>
<div id="sourceList" class="source-list"></div>
</aside>
<section class="recipes-panel panel">
<div class="panel-head">
<div>
<p class="section-label">One-click Recipes</p>
<h3>Goal-Based Workflows</h3>
</div>
<button class="tiny-button" id="recipeHelpButton" type="button">Runner guide</button>
</div>
<div id="recipeList" class="recipe-list"></div>
</section>
<aside class="studio-panel panel">
<div class="panel-head">
<div>
<p class="section-label">Native Studio</p>
<h3>NotebookLM Native Tools</h3>
</div>
<span class="count-badge" id="artifactCountBadge">—</span>
</div>
<div id="studioGrid" class="studio-grid"></div>
</aside>
</div>
<section class="timeline-panel panel">
<div class="panel-head">
<div>
<p class="section-label">Status Timeline</p>
<h3>Generation Status and Output Files</h3>
</div>
<button class="tiny-button" id="clearJobsButton" type="button">Clear completed jobs</button>
</div>
<div id="timeline" class="timeline"></div>
</section>
</section>
</main>
<div class="command-overlay" id="commandOverlay" aria-hidden="true">
<div class="command-palette">
<div class="command-input">
<span class="icon-search"></span>
<input id="commandInput" placeholder="Search notebooks or type: untitled / video / 2026-03 / AI Agent" />
</div>
<div id="commandResults" class="command-results"></div>
</div>
</div>
<template id="notebookCardTemplate">
<button class="notebook-card" type="button">
<div class="card-topline">
<span class="status-badge">Ready</span>
<span class="date-chip"></span>
</div>
<h3></h3>
<p class="id-line"></p>
<div class="card-metrics">
<span><strong data-field="source-count">—</strong> sources</span>
<span><strong data-field="artifact-count">—</strong> artifacts</span>
<span><strong data-field="last-activity">—</strong> activity</span>
</div>
<div class="artifact-strip"></div>
<div class="card-actions">
<span>Open Data Room</span>
<span class="icon-arrow"></span>
</div>
</button>
</template>
<script src="/app.js"></script>
</body>
</html>
:root {
color-scheme: dark;
--bg: #10100f;
--bg-2: #161614;
--surface: rgba(247, 246, 238, 0.08);
--surface-strong: rgba(247, 246, 238, 0.13);
--surface-soft: rgba(247, 246, 238, 0.055);
--text: #f7f3e8;
--muted: rgba(247, 243, 232, 0.62);
--faint: rgba(247, 243, 232, 0.38);
--border: rgba(247, 243, 232, 0.14);
--accent: #b8f56d;
--accent-2: #73e0d4;
--accent-3: #ff8d6c;
--danger: #ff6f83;
--warning: #ffd166;
--ok: #9cf27d;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.32);
--radius: 22px;
--font-display: "New York", "Iowan Old Style", Georgia, serif;
--font-ui: "Avenir Next", "SF Pro Display", "Helvetica Neue", sans-serif;
}
body[data-theme="pearl"] {
color-scheme: light;
--bg: #f4f5f0;
--bg-2: #ebece4;
--surface: rgba(255, 255, 255, 0.76);
--surface-strong: rgba(255, 255, 255, 0.94);
--surface-soft: rgba(255, 255, 255, 0.55);
--text: #171813;
--muted: rgba(23, 24, 19, 0.62);
--faint: rgba(23, 24, 19, 0.38);
--border: rgba(23, 24, 19, 0.12);
--accent: #205d4f;
--accent-2: #b0522f;
--accent-3: #7d5ef2;
--shadow: 0 24px 80px rgba(60, 66, 48, 0.16);
}
body[data-theme="signal"] {
--bg: #111111;
--bg-2: #171514;
--surface: rgba(255, 255, 255, 0.075);
--surface-strong: rgba(255, 255, 255, 0.13);
--surface-soft: rgba(255, 255, 255, 0.05);
--text: #fffaf2;
--muted: rgba(255, 250, 242, 0.65);
--accent: #ffcc32;
--accent-2: #3ce3ff;
--accent-3: #ff5a88;
}
* {
box-sizing: border-box;
}
html {
height: 100%;
overflow-x: hidden;
overflow-y: hidden;
}
body {
margin: 0;
height: 100%;
background:
radial-gradient(circle at 16% 5%, color-mix(in srgb, var(--accent) 24%, transparent), transparent 26rem),
radial-gradient(circle at 88% 12%, color-mix(in srgb, var(--accent-2) 20%, transparent), transparent 24rem),
linear-gradient(135deg, var(--bg), var(--bg-2));
color: var(--text);
font-family: var(--font-ui);
letter-spacing: 0;
overflow-x: clip;
overflow-y: hidden;
}
button,
input {
font: inherit;
}
button {
color: inherit;
}
.ambient {
position: fixed;
pointer-events: none;
filter: blur(46px);
opacity: 0.54;
border-radius: 999px;
transform: translateZ(0);
animation: breathe 8s ease-in-out infinite;
}
.ambient-one {
width: 19rem;
height: 19rem;
right: 12vw;
top: 9vh;
background: color-mix(in srgb, var(--accent) 34%, transparent);
}
.ambient-two {
width: 17rem;
height: 17rem;
left: 8vw;
bottom: 5vh;
background: color-mix(in srgb, var(--accent-3) 25%, transparent);
animation-delay: -3s;
}
.grain {
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.11;
mix-blend-mode: overlay;
background-image:
linear-gradient(90deg, rgba(255, 255, 255, 0.07) 1px, transparent 1px),
linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px);
background-size: 72px 72px;
mask-image: linear-gradient(to bottom, black, transparent 85%);
}
.app-shell {
position: relative;
z-index: 1;
width: min(1480px, calc(100vw - 40px));
height: 100vh;
margin: 0 auto;
padding: 22px 0 44px;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior-y: contain;
scrollbar-gutter: stable;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
min-height: 70px;
}
.brand,
.ghost-button,
.back-button,
.tiny-button,
.theme-switcher button,
.filter-pills button {
border: 1px solid var(--border);
background: var(--surface);
backdrop-filter: blur(22px);
border-radius: 999px;
cursor: pointer;
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
}
.brand:hover,
.ghost-button:hover,
.back-button:hover,
.tiny-button:hover,
.filter-pills button:hover {
transform: translateY(-1px);
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
}
.brand {
display: inline-flex;
align-items: center;
gap: 13px;
padding: 8px 16px 8px 9px;
text-align: left;
}
.brand-mark {
display: grid;
place-items: center;
width: 44px;
height: 44px;
padding: 7px;
border-radius: 16px;
background: linear-gradient(145deg, var(--accent), var(--accent-2));
box-shadow: 0 14px 34px color-mix(in srgb, var(--accent) 24%, transparent);
overflow: hidden;
}
.brand-mark img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
.brand strong {
display: block;
font-size: 15px;
line-height: 1.1;
}
.brand small,
.runner-card small {
display: block;
color: var(--muted);
font-size: 12px;
margin-top: 3px;
}
.top-actions {
display: flex;
align-items: center;
gap: 12px;
}
.ghost-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 11px 13px;
font-size: 13px;
}
kbd {
border: 1px solid var(--border);
border-radius: 7px;
padding: 1px 6px;
color: var(--muted);
font-size: 11px;
background: var(--surface-soft);
}
.theme-switcher {
display: flex;
gap: 5px;
padding: 5px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--surface-soft);
backdrop-filter: blur(24px);
}
.theme-switcher button {
border: 0;
background: transparent;
padding: 8px 11px;
color: var(--muted);
font-size: 12px;
}
.theme-switcher button.active {
color: var(--text);
background: var(--surface-strong);
box-shadow: inset 0 0 0 1px var(--border);
}
.view {
display: none;
animation: liftIn 420ms ease both;
}
.view.active {
display: block;
}
.hero-panel {
display: grid;
grid-template-columns: minmax(0, 1.3fr) minmax(290px, 0.7fr);
gap: 24px;
align-items: stretch;
margin: 18px 0 22px;
padding: 34px;
border: 1px solid var(--border);
border-radius: 32px;
background:
linear-gradient(135deg, var(--surface-strong), var(--surface-soft)),
radial-gradient(circle at 82% 18%, color-mix(in srgb, var(--accent) 20%, transparent), transparent 20rem);
box-shadow: var(--shadow);
overflow: hidden;
}
.section-label {
margin: 0 0 8px;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 11px;
font-weight: 700;
}
h1,
h2,
h3 {
margin: 0;
font-family: var(--font-display);
letter-spacing: 0;
}
h1 {
max-width: 820px;
font-size: clamp(42px, 7vw, 86px);
line-height: 0.94;
font-weight: 600;
}
.hero-panel h1 {
max-width: 760px;
font-family: var(--font-ui);
font-size: 72px;
line-height: 1.06;
font-weight: 800;
text-wrap: balance;
}
h2 {
font-size: clamp(30px, 3.6vw, 48px);
line-height: 1;
}
h3 {
font-size: 23px;
line-height: 1.08;
}
.hero-stats {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.hero-stats div,
.panel,
.notebook-card,
.runner-card {
border: 1px solid var(--border);
background: var(--surface);
backdrop-filter: blur(26px);
box-shadow: var(--shadow);
}
.hero-stats div {
padding: 18px;
border-radius: 20px;
}
.hero-stats strong {
display: block;
font-size: 32px;
line-height: 1;
}
.hero-stats span {
display: block;
margin-top: 6px;
color: var(--muted);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.library-toolbar {
display: grid;
grid-template-columns: minmax(240px, 1fr) auto;
gap: 14px;
align-items: center;
margin-bottom: 18px;
}
.search-box,
.command-input {
display: flex;
align-items: center;
gap: 10px;
min-height: 48px;
padding: 0 15px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--surface);
backdrop-filter: blur(24px);
}
.search-box input,
.command-input input {
width: 100%;
border: 0;
outline: 0;
background: transparent;
color: var(--text);
font-size: 14px;
}
.search-box input::placeholder,
.command-input input::placeholder {
color: var(--faint);
}
.filter-pills {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}
.filter-pills button {
padding: 10px 13px;
color: var(--muted);
font-size: 13px;
}
.filter-pills button.active {
color: #12130f;
background: var(--accent);
}
.notebook-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.notebook-card {
position: relative;
display: flex;
flex-direction: column;
gap: 14px;
min-height: 260px;
padding: 18px;
border-radius: 24px;
text-align: left;
overflow: hidden;
}
.notebook-card::before {
content: "";
position: absolute;
inset: -1px;
opacity: 0;
background: radial-gradient(circle at 20% 0%, color-mix(in srgb, var(--accent) 24%, transparent), transparent 15rem);
transition: opacity 200ms ease;
}
.notebook-card > * {
position: relative;
}
.notebook-card:hover {
transform: translateY(-4px);
border-color: color-mix(in srgb, var(--accent) 42%, var(--border));
}
.notebook-card:hover::before {
opacity: 1;
}
.card-topline,
.card-actions,
.detail-header,
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.status-badge,
.date-chip,
.count-badge,
.artifact-pill {
display: inline-flex;
align-items: center;
min-height: 26px;
padding: 0 9px;
border: 1px solid var(--border);
border-radius: 999px;
color: var(--muted);
background: var(--surface-soft);
font-size: 11px;
white-space: nowrap;
}
.status-ready {
color: var(--ok);
}
.status-warning {
color: var(--warning);
}
.status-failed {
color: var(--danger);
}
.notebook-card h3 {
display: -webkit-box;
min-height: 54px;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.id-line {
margin: 0;
color: var(--faint);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.card-metrics span {
min-height: 54px;
padding: 10px;
border: 1px solid var(--border);
border-radius: 15px;
color: var(--muted);
background: var(--surface-soft);
font-size: 11px;
}
.card-metrics strong {
display: block;
color: var(--text);
font-size: 17px;
}
.artifact-strip {
display: flex;
flex-wrap: wrap;
gap: 6px;
min-height: 28px;
}
.artifact-pill.done {
color: var(--accent);
border-color: color-mix(in srgb, var(--accent) 34%, var(--border));
}
.card-actions {
margin-top: auto;
color: var(--accent);
font-weight: 700;
font-size: 13px;
}
.detail-view {
padding-top: 18px;
}
.detail-header {
display: grid;
grid-template-columns: auto minmax(0, 1fr) minmax(260px, auto);
align-items: center;
}
.detail-header h2 {
max-width: 980px;
line-height: 0.96;
}
.back-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 11px 13px;
}
.muted {
color: var(--muted);
}
.runner-card {
display: flex;
align-items: center;
gap: 10px;
min-width: 260px;
padding: 12px 14px;
border-radius: 18px;
}
.pulse-dot {
width: 11px;
height: 11px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent);
animation: pulse 1.8s ease infinite;
}
.data-room {
display: grid;
grid-template-columns: minmax(240px, 0.74fr) minmax(520px, 1.55fr) minmax(320px, 0.95fr);
gap: 16px;
margin-top: 18px;
align-items: start;
}
.panel {
border-radius: 26px;
padding: 18px;
}
.data-room > .panel {
display: flex;
flex-direction: column;
height: clamp(560px, calc(100vh - 252px), 720px);
min-height: 0;
}
.panel-head {
flex: 0 0 auto;
}
.source-list,
.recipe-list,
.studio-grid,
.timeline {
margin-top: 16px;
}
.source-list {
display: grid;
gap: 8px;
min-height: 0;
overflow-y: auto;
padding-right: 3px;
scrollbar-gutter: stable;
}
.source-item,
.recipe-card,
.studio-button,
.job-card,
.command-result {
border: 1px solid var(--border);
background: var(--surface-soft);
border-radius: 18px;
}
.source-item {
padding: 11px 12px;
}
.source-item strong,
.job-card strong {
display: block;
line-height: 1.25;
}
.source-item strong {
display: -webkit-box;
font-size: 14px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.source-item span,
.source-item small,
.job-card small {
display: block;
margin-top: 6px;
color: var(--muted);
font-size: 12px;
}
.recipe-list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
grid-auto-rows: minmax(136px, auto);
align-items: stretch;
min-height: 0;
overflow-y: auto;
padding-right: 3px;
scrollbar-gutter: stable;
}
.recipe-card {
position: relative;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
min-height: 136px;
padding: 11px;
overflow: hidden;
}
.recipe-card::after {
content: "";
position: absolute;
right: -40px;
top: -50px;
width: 118px;
height: 118px;
border-radius: 50%;
background: color-mix(in srgb, var(--accent-2) 18%, transparent);
}
.recipe-card h4,
.studio-button h4 {
position: relative;
margin: 0 0 6px;
font-size: 14px;
line-height: 1.15;
}
.recipe-card p {
display: none;
}
.recipe-card .output-chips {
align-content: start;
margin: 6px 0 9px;
min-height: 0;
}
.output-chips {
position: relative;
display: flex;
flex-wrap: wrap;
gap: 5px;
margin: 6px 0 10px;
}
.output-chips span {
padding: 5px 7px;
border: 1px solid var(--border);
border-radius: 999px;
color: var(--muted);
font-size: 10px;
background: var(--surface);
}
.run-button,
.studio-button {
cursor: pointer;
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
}
.run-button {
position: relative;
width: 100%;
align-self: end;
min-height: 34px;
border: 0;
border-radius: 13px;
padding: 8px 10px;
color: #11130f;
background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent-2) 68%, var(--accent)));
font-weight: 800;
}
.run-button:hover,
.studio-button:hover {
transform: translateY(-2px);
}
.studio-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
min-height: 0;
overflow-y: auto;
padding-right: 3px;
scrollbar-gutter: stable;
}
.studio-button {
display: grid;
grid-template-columns: minmax(0, 1fr) 34px;
grid-template-rows: auto minmax(0, 1fr);
align-items: start;
gap: 8px;
width: 100%;
min-height: 94px;
padding: 11px;
text-align: left;
background:
radial-gradient(circle at 100% 0%, color-mix(in srgb, var(--tool-tint) 18%, transparent), transparent 7rem),
color-mix(in srgb, var(--tool-bg) 38%, var(--surface-soft));
}
.studio-icon {
display: block;
width: 25px;
height: 25px;
color: var(--tool-tint);
background: transparent;
grid-column: 1;
grid-row: 1;
}
.studio-icon svg {
display: block;
width: 100%;
height: 100%;
fill: none;
stroke: currentColor;
stroke-width: 1.9;
stroke-linecap: round;
stroke-linejoin: round;
}
.studio-button h4 {
margin: 0;
font-size: 13px;
line-height: 1.12;
}
.studio-copy {
grid-column: 1;
grid-row: 2;
min-width: 0;
}
.studio-description {
display: none;
color: var(--muted);
font-size: 10.5px;
line-height: 1.25;
}
.tool-arrow {
display: grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: 50%;
color: var(--text);
background: color-mix(in srgb, var(--text) 9%, transparent);
font-size: 24px;
line-height: 1;
grid-column: 2;
grid-row: 1 / span 2;
justify-self: end;
align-self: center;
}
.studio-button[data-tool-id="audio"] {
--tool-tint: #e6ddff;
--tool-bg: #59536f;
}
.studio-button[data-tool-id="slide-deck"] {
--tool-tint: #fff4a6;
--tool-bg: #5d5940;
}
.studio-button[data-tool-id="video"] {
--tool-tint: #cdf6df;
--tool-bg: #405d4b;
}
.studio-button[data-tool-id="mind-map"] {
--tool-tint: #ffd0f2;
--tool-bg: #604356;
}
.studio-button[data-tool-id="report"] {
--tool-tint: #eef5be;
--tool-bg: #565b3d;
}
.studio-button[data-tool-id="flashcards"] {
--tool-tint: #ffd8cf;
--tool-bg: #604a43;
}
.studio-button[data-tool-id="quiz"] {
--tool-tint: #c8f7ff;
--tool-bg: #40515d;
}
.studio-button[data-tool-id="infographic"] {
--tool-tint: #f2c7ee;
--tool-bg: #604156;
}
.studio-button[data-tool-id="data-table"] {
--tool-tint: #dadcff;
--tool-bg: #494c63;
}
.timeline-panel {
margin-top: 14px;
}
.timeline {
display: grid;
gap: 7px;
}
.job-card {
display: grid;
gap: 6px;
padding: 9px 10px 10px;
font-family: "Sohne", "Avenir Next", "SF Pro Text", "Helvetica Neue", sans-serif;
border-radius: 16px;
}
.job-card strong {
font-size: 12.5px;
}
.job-header-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(172px, auto);
gap: 10px;
align-items: start;
}
.job-title-block {
min-width: 0;
}
.job-title-block strong,
.job-title-block small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.job-title-block small {
margin-top: 3px;
}
.job-body {
min-width: 0;
}
.job-card .output-chips {
margin: 3px 0 5px;
}
.job-card-single .output-chips {
display: none;
}
.job-status-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(172px, 0.16fr);
gap: 10px;
align-items: start;
}
.job-card-single .job-status-grid {
margin-top: 2px;
}
.progress-track {
height: 5px;
margin-top: 6px;
border-radius: 99px;
background: var(--surface-soft);
overflow: hidden;
}
.preview-warning {
margin-top: 10px;
padding: 9px 10px;
border: 1px solid color-mix(in srgb, var(--warning) 38%, var(--border));
border-radius: 12px;
color: var(--warning);
background: color-mix(in srgb, var(--warning) 10%, transparent);
font-size: 12px;
}
.output-list {
display: grid;
gap: 5px;
margin-top: 0;
}
.output-list span {
display: grid;
grid-template-columns: minmax(120px, 0.35fr) minmax(0, 1fr);
gap: 8px;
align-items: center;
min-height: 26px;
padding: 3px 8px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--surface-soft);
}
.output-list strong {
color: var(--text);
font-size: 11.5px;
}
.output-list em {
min-width: 0;
color: var(--muted);
font-size: 10.5px;
font-style: normal;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.job-action-panel {
display: grid;
justify-items: end;
min-width: 0;
}
.job-action-panel small {
display: none;
max-width: 230px;
min-width: 0;
color: var(--faint);
font-size: 10px;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: right;
}
.handoff-button {
border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--border));
border-radius: 999px;
padding: 7px 11px;
color: #10120e;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
cursor: pointer;
font-size: 12px;
font-weight: 900;
line-height: 1;
white-space: nowrap;
}
.job-card-summary {
padding: 9px 10px;
}
.job-summary-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(180px, auto);
gap: 12px;
align-items: center;
}
.job-summary-status {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 9px;
min-width: 0;
color: var(--muted);
font-size: 11px;
font-weight: 800;
white-space: nowrap;
}
.job-summary-status em {
max-width: 120px;
color: var(--faint);
font-size: 10.5px;
font-style: normal;
overflow: hidden;
text-overflow: ellipsis;
}
.handoff-button:disabled {
color: var(--muted);
background: var(--surface-soft);
border-color: var(--border);
cursor: not-allowed;
opacity: 0.58;
}
.prompt-overlay {
position: fixed;
inset: 0;
z-index: 20;
display: none;
place-items: center;
padding: 32px;
background: rgba(0, 0, 0, 0.58);
backdrop-filter: blur(20px);
overflow: auto;
}
.prompt-overlay.active {
display: grid;
}
.prompt-panel {
display: grid;
grid-template-rows: auto auto minmax(260px, 1fr) auto;
gap: 14px;
width: min(920px, calc(100vw - 64px));
max-height: calc(100vh - 64px);
padding: 18px;
border: 1px solid var(--border);
border-radius: 22px;
background: color-mix(in srgb, var(--bg-2) 91%, transparent);
box-shadow: 0 28px 120px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
.prompt-panel-head,
.prompt-footer {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
min-width: 0;
}
.prompt-panel h3,
.prompt-status {
margin: 0;
}
.prompt-status,
.prompt-footer small {
color: var(--muted);
font-size: 12px;
}
.prompt-textarea {
display: block;
width: 100%;
min-width: 0;
height: clamp(320px, 52vh, 520px);
resize: none;
border: 1px solid var(--border);
border-radius: 14px;
padding: 14px;
color: var(--text);
background: var(--surface-soft);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
line-height: 1.55;
overflow: auto;
}
.prompt-footer small {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.prompt-footer div {
display: flex;
gap: 8px;
flex: 0 0 auto;
}
@media (max-width: 720px) {
.prompt-overlay {
padding: 16px;
}
.prompt-panel {
width: calc(100vw - 32px);
max-height: calc(100vh - 32px);
}
.prompt-panel-head,
.prompt-footer {
align-items: flex-start;
flex-direction: column;
}
.prompt-footer small {
white-space: normal;
}
}
.progress-track i {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
}
.step-list {
display: grid;
gap: 5px;
min-width: 0;
}
.step-list span {
display: flex;
align-items: center;
min-height: 26px;
padding: 0 0 0 2px;
color: var(--muted);
font-size: 10.5px;
font-weight: 700;
line-height: 1.15;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tiny-button {
padding: 8px 10px;
font-size: 12px;
color: var(--muted);
}
.command-overlay {
position: fixed;
inset: 0;
z-index: 10;
display: none;
place-items: start center;
padding-top: 9vh;
background: rgba(0, 0, 0, 0.46);
backdrop-filter: blur(18px);
}
.command-overlay.active {
display: grid;
}
.command-palette {
width: min(760px, calc(100vw - 36px));
border: 1px solid var(--border);
border-radius: 26px;
background: color-mix(in srgb, var(--bg-2) 82%, transparent);
box-shadow: 0 28px 120px rgba(0, 0, 0, 0.45);
padding: 12px;
}
.command-input {
border-radius: 18px;
}
.command-results {
display: grid;
gap: 8px;
margin-top: 10px;
max-height: 50vh;
overflow: auto;
}
.command-result {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
padding: 12px;
cursor: pointer;
}
.command-result strong {
display: block;
}
.command-result small {
color: var(--muted);
}
.icon-search,
.icon-arrow {
display: inline-block;
width: 16px;
height: 16px;
}
.icon-search {
border: 2px solid currentColor;
border-radius: 50%;
position: relative;
opacity: 0.75;
}
.icon-search::after {
content: "";
position: absolute;
width: 7px;
height: 2px;
right: -5px;
bottom: -2px;
border-radius: 4px;
background: currentColor;
transform: rotate(45deg);
}
.icon-arrow::before {
content: "→";
font-size: 17px;
}
.icon-arrow-left::before {
content: "←";
}
@keyframes liftIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes breathe {
50% {
transform: scale(1.08) translate3d(2vw, -1vh, 0);
}
}
@keyframes pulse {
70% {
box-shadow: 0 0 0 12px color-mix(in srgb, var(--accent) 0%, transparent);
}
}
@media (max-width: 1100px) {
.hero-panel h1 {
font-size: 58px;
}
.detail-header {
grid-template-columns: 1fr;
}
.notebook-grid,
.data-room {
grid-template-columns: 1fr 1fr;
}
.data-room > .panel {
height: auto;
max-height: none;
}
.recipes-panel {
grid-column: 1 / -1;
order: -1;
}
.source-list,
.recipe-list,
.studio-grid {
max-height: 520px;
}
}
@media (max-width: 780px) {
.app-shell {
width: min(100vw - 24px, 720px);
}
.topbar,
.detail-header,
.library-toolbar,
.hero-panel {
grid-template-columns: 1fr;
flex-direction: column;
align-items: stretch;
}
.top-actions,
.filter-pills {
justify-content: flex-start;
}
.notebook-grid,
.data-room,
.recipe-list,
.studio-grid,
.job-card {
grid-template-columns: 1fr;
}
.hero-panel {
padding: 24px;
}
.hero-panel h1 {
font-size: 42px;
line-height: 1.04;
}
}
@media (max-width: 560px) {
.job-header-row,
.job-status-grid,
.job-summary-row {
grid-template-columns: 1fr;
}
.job-action-panel {
justify-items: start;
}
.job-action-panel small {
max-width: 100%;
text-align: left;
}
.job-summary-status {
justify-content: flex-start;
}
}
MIT License
Copyright (c) 2026 Toolsai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
NotebookLM Studio Skill
繁體中文 README
Turn Codex into your NotebookLM research operator: prepare sources, create or reuse NotebookLM notebooks, generate NotebookLM Studio artifacts, download the outputs locally, and hand everything back to Codex for analysis, rewriting, study guides, decks, reports, and content workflows.
NotebookLM Studio Skill does not replace NotebookLM. It connects NotebookLM to an AI agent workflow so Codex can help you operate the research process end to end.
---
What It Does
NotebookLM Studio Skill helps you turn source material into a complete research workflow:
- Create or reuse NotebookLM notebooks
- Organize and add NotebookLM-supported sources such as web pages, PDFs, documents, slide decks, tables, audio, video, and images
- Trigger NotebookLM Studio artifacts
- Download generated outputs to a local folder
- Create handoff files so Codex can read and analyze the results directly
- Launch a local Dashboard control room
- Convert NotebookLM Mind Map JSON into interactive HTML
- Turn NotebookLM outputs into reports, tutorials, presentation outlines, content strategies, or research summaries
In short:
Sources
→ NotebookLM notebook
→ NotebookLM Studio artifacts
→ Local downloads
→ Codex analysis, rewriting, and follow-up production---
Screenshots
Codex + NotebookLM Studio Side by Side
!Codex and NotebookLM Studio side by side
Codex can read handoff files and analyze NotebookLM outputs on one side, while the NotebookLM Studio Dashboard manages notebooks, sources, recipes, and generation jobs on the other.
Notebook Library
!Notebook library
The Library view lets you browse NotebookLM notebooks, source counts, generated artifacts, recent activity, and readiness status.
Notebook Data Room
!Notebook data room
The Data Room is the workspace for a single notebook: sources on the left, goal-based workflows in the center, and native NotebookLM tools on the right.
One-Click Workflows
The One-Click Workflows package common research jobs into ready-made flows. Instead of manually generating each artifact one by one, choose a goal and let the Skill create a useful set of NotebookLM outputs.
| Workflow | Best for |
|---|---|
| Executive Briefing Suite | Decision-ready briefing report, slide deck, data table, and audio overview. |
| Visual Story Suite | Turning research into video overview, slide deck, infographic, and mind map. |
| Learning Masterpack | Study guide, quiz, flashcards, mind map, and audio overview for learning. |
| Competitive Intel Suite | Custom report, data table, slide deck, and infographic for market or competitor research. |
| Due Diligence Review Suite | Custom report, data table, mind map, and quiz for review workflows. |
| Content Distribution Suite | Blog report, video overview, infographic, and slide deck for publishing. |
| Research Synthesis Suite | Research report, mind map, data table, and audio overview for multi-source synthesis. |
| Stakeholder Q&A Suite | Briefing report, slide deck, quiz, and flashcards for meetings or alignment. |
| Knowledge Base Digest Suite | Digest report, data table, mind map, and audio overview for knowledge bases. |
Job Timeline and Codex Handoff
!Job timeline and Codex handoff
The timeline tracks generation status, artifact type, downloaded outputs, and the handoff path back into Codex.
Generated Slide Deck / PDF + Codex Analysis
!Slide deck PDF display with Codex analysis
NotebookLM-generated slide decks or PDFs can be downloaded locally and reviewed side by side with Codex analysis.
---
How Is This Different From Using NotebookLM Directly?
NotebookLM already supports many source formats and can generate useful outputs. This Skill adds the workflow layer around it.
| Need | NotebookLM directly | With this Skill |
|---|---|---|
| Create notebooks | Manual | Assisted by Codex |
| Add many sources | Manual, one by one | Batch organization and checks |
| Generate artifacts | Manual clicking | Prompt or Dashboard driven |
| Download outputs | Manual | Downloaded into local folders |
| Analyze with Codex | Copy and organize yourself | Handoff files are created automatically |
| Mind Map JSON | Raw JSON | Interactive HTML conversion |
| Research tracking | Manual notes | Job timeline and manifest |
| Multi-step workflows | Human-operated | Codex + NotebookLM handoff loop |
Think of NotebookLM as the research model that reads a large body of material. This Skill is the operator layer that prepares sources, runs the workflow, downloads outputs, and hands them back to Codex.
---
Installation
You can install the Skill in either of these ways.
Option 1: npx skills add
If your agent or Skills manager supports skills add, run:
npx skills add Toolsai/notebooklm-studio-SkillThis is the simplest option for users already working with Skills.
Option 2: git clone
Clone the repository:
git clone https://github.com/Toolsai/notebooklm-studio-Skill.gitThen place the folder where your agent can read Skills.
For Codex:
mkdir -p ~/.codex/skills
cp -R notebooklm-studio-Skill ~/.codex/skills/notebooklm-studioFor Claude Code:
mkdir -p ~/.claude/skills
cp -R notebooklm-studio-Skill ~/.claude/skills/notebooklm-studioFor other agents, use the Skills directory required by that tool.
---
Quick Start
After installation, ask Codex:
Initiate /notebookLM studioCodex will follow the Skill instructions, check the environment, confirm NotebookLM availability, and guide you through Google login or authorization when needed. Login and security verification must be completed by the user.
---
Launch the Dashboard
Ask your agent:
Launch the NotebookLM Studio dashboard.The agent will provide a local URL, for example:
http://localhost:8765/If the default port is already in use, the server will use the next available local port and report the actual URL.
---
Prompt Examples
Create a Research Notebook
Use /notebookLM studio to create a new NotebookLM notebook called "AI Agent Research Pack", add the following URLs as sources, generate a Report and Mind Map, download the outputs, and analyze them for me.Turn Sources Into a Study Pack
Use /notebookLM studio to turn these sources into a study pack: Report, Quiz, Flashcards, and Mind Map. Download all outputs and summarize the key ideas.Generate a Slide Deck
Use /notebookLM studio to create a Slide Deck from this NotebookLM notebook. Download PDF and PPTX if available, then give me a presentation outline.Generate an Audio Overview
Use /notebookLM studio to generate an Audio Overview from this notebook. Download it and tell me what topics it covers.Generate a Mind Map and HTML View
Generate a NotebookLM Mind Map, download the JSON, convert it into interactive HTML, and give me both file links.Search Sources and Build a NotebookLM Pack
Search for the most reliable OpenAI Codex Agent tutorials, prioritize official documentation and high-quality guides, create a new NotebookLM notebook, add the sources, then use NotebookLM to generate a deep tutorial report.---
Supported NotebookLM Studio Artifacts
| Artifact | Use |
|---|---|
| Report | Briefing, study guide, blog post, or custom report |
| Mind Map | Concept graph, with optional interactive HTML conversion |
| Audio Overview | Podcast-style audio summary |
| Video Overview | NotebookLM video summary |
| Slide Deck | Presentation deck |
| Infographic | Visual summary |
| Quiz | Assessment questions |
| Flashcards | Study cards |
| Data Table | Structured extraction or comparison table |
---
Supported Sources
This Skill prioritizes source formats supported by NotebookLM, including PDF, TXT, Markdown, DOCX, CSV, PPTX, EPUB, audio, video, and images.
Its main role is source organization, import checks, generation tracking, output downloads, handoff creation, and follow-up Codex analysis.
---
Use Cases
Research Assistant
Find sources → build NotebookLM → generate Report → Codex performs deeper analysisStudy Coach
Course material → Study Guide → Quiz → Flashcards → Codex designs practiceContent Factory
Sources → NotebookLM Report / Mind Map → Codex rewrites into posts, scripts, or decksTeam Knowledge Base
Docs / PRs / meeting notes → NotebookLM → Data Table / Summary / Action Plan---
Compatibility
This Skill is designed primarily for Codex workflows, especially local file handling, Dashboard operation, handoff analysis, and multi-step research tasks.
Other agents that support Skills or local tool execution can adapt the project, but may require platform-specific adjustments.
---
Safety Boundaries
Use this project only with accounts, data, and content you are authorized to access.
- Do not use it to bypass paywalls, DRM, login walls, private access controls, or content restrictions
- Do not commit private login material, sessions, tokens, or sensitive local outputs to GitHub
- Google login and security verification must be completed by the user
- NotebookLM is a Google product. This project is not affiliated with or endorsed by Google
---
License
This project is licensed under the MIT License. See LICENSE.
NotebookLM Studio Skill
English README
把 Codex 變成你的 NotebookLM 研究助理:協助整理來源、建立 NotebookLM 筆記本、生成 NotebookLM Studio 內容,並把結果下載到本地,方便繼續用 Codex 分析、改寫、整理或製作輸出。
這是一個為 AI agent 工作流設計的 NotebookLM 輔助 Skill。它不是取代 NotebookLM,而是把 NotebookLM 接到 Codex 的研究、寫作、學習和內容製作流程裡。
---
這個 Skill 可以做什麼?
NotebookLM Studio Skill 可以幫你把資料變成完整研究工作流:
- 建立或重用 NotebookLM 筆記本
- 整理並加入 NotebookLM 支援的來源,例如網頁、PDF、文件、簡報、表格、音訊、影片和圖片
- 觸發 NotebookLM Studio 生成內容
- 下載生成結果到本地資料夾
- 建立 handoff 檔案,讓 Codex 可以直接讀取並分析
- 啟動本地 Dashboard 操作面板
- 把 NotebookLM Mind Map 轉成可互動 HTML
- 將 NotebookLM 生成結果整理成報告、教學、簡報大綱、內容策略或研究摘要
簡單來說:
資料來源
→ NotebookLM 筆記本
→ NotebookLM Studio 生成內容
→ 本地下載
→ Codex 分析、整理、二次創作---
操作畫面
Codex + NotebookLM Studio 並排工作流
!Codex and NotebookLM Studio side by side
Codex 可以在左邊讀取 handoff、分析 NotebookLM 生成結果;NotebookLM Studio Dashboard 則在右邊負責管理筆記本、來源、recipes 和生成任務。
Notebook Library
!Notebook library
Library 畫面用來快速瀏覽所有 NotebookLM 筆記本,包括來源數量、已生成 artifact、最近活動時間和狀態。
Notebook Data Room
!Notebook data room
Data Room 是單一 notebook 的工作台:左側是來源,中間是 goal-based workflows,右側是 NotebookLM 原生工具。適合從同一批資料出發,快速生成報告、簡報、影片概覽、Mind Map、Quiz 或資料表。
One-Click Workflows
Data Room 中間的 One-Click Workflows 會把常見研究任務包成一鍵流程。你不用逐個 artifact 手動生成,只要選擇目標,Skill 會按流程建立一組合適的 NotebookLM 輸出。
| Workflow | 適合用途 |
|---|---|
| Executive Briefing Suite | 為決策者快速生成 briefing report、slide deck、data table 和 audio overview。 |
| Visual Story Suite | 把資料變成視覺敘事,適合 video overview、slide deck、infographic 和 mind map。 |
| Learning Masterpack | 建立學習套裝,包含 study guide、quiz、flashcards、mind map 和 audio overview。 |
| Competitive Intel Suite | 整理競爭情報,輸出 custom report、data table、slide deck 和 infographic。 |
| Due Diligence Review Suite | 做審查和盡職調查,生成 custom report、data table、mind map 和 quiz。 |
| Content Distribution Suite | 把研究內容拆成可發布素材,例如 blog report、video overview、infographic 和 slide deck。 |
| Research Synthesis Suite | 將多來源資料整合成 research report、mind map、data table 和 audio overview。 |
| Stakeholder Q&A Suite | 為會議和簡報準備 briefing report、slide deck、quiz 和 flashcards。 |
| Knowledge Base Digest Suite | 把知識庫壓縮成 digest report、data table、mind map 和 audio overview。 |
Job Timeline 與 Codex Handoff
!Job timeline and Codex handoff
Timeline 會追蹤每個生成任務的狀態、artifact 類型、下載結果和後續交給 Codex 分析的入口。
生成結果預覽:Slide Deck / PDF / Codex 分析
!Slide deck PDF display with Codex analysis
NotebookLM 生成的 Slide Deck 或 PDF 可以下載到本地,並和 Codex 的分析結果並排查看。這讓你不只拿到 artifact,也能立刻請 Codex 檢查重點、整理結論、指出風險與提出下一步建議。
---
和直接使用 NotebookLM 有什麼不同?
NotebookLM 官方已經可以接收很多格式,也可以生成很多內容。這個 Skill 的價值不是重做 NotebookLM 已經做得很好的事,而是補上「工作流」。
| 需求 | 直接使用 NotebookLM | 使用這個 Skill |
|---|---|---|
| 建立筆記本 | 手動操作 | 由 Codex 協助建立 |
| 加入多個來源 | 手動逐個加入 | 可批量整理、檢查、加入 |
| 生成內容 | 手動點選 | 可用提示詞或 Dashboard 觸發 |
| 下載結果 | 手動處理 | 自動下載到本地 |
| 交給 Codex 分析 | 需要自己複製整理 | 自動產生 handoff 檔案 |
| Mind Map JSON | 原始 JSON | 可轉成互動 HTML |
| 研究流程追蹤 | 手動記錄 | job timeline + manifest |
| 多步驟研究任務 | 人手串接 | Codex + NotebookLM 串接 |
你可以把 NotebookLM 想成「很會讀資料的研究模型」,而這個 Skill 是「幫你整理資料、按流程操作、下載結果、交回 Agent 的研究助理」。
---
安裝方式
你可以用以下其中一種方式安裝這個 Skill。
方法一:使用 npx skills add
如果你的 Agent 或 Skills 管理工具支援 skills add,可以直接安裝:
npx skills add Toolsai/notebooklm-studio-Skill這是最簡單的安裝方式,適合已經使用 Skills 工作流的人。
方法二:使用 git clone
你也可以用傳統方式下載:
git clone https://github.com/Toolsai/notebooklm-studio-Skill.git然後把下載後的資料夾放到你的 Agent 可以讀取 Skills 的位置。建議安裝時把目標資料夾命名為 notebooklm-studio,這樣之後呼叫 Skill 時會更清楚。
以 Codex 常見的本地 Skills 目錄為例:
mkdir -p ~/.codex/skills
cp -R notebooklm-studio-Skill ~/.codex/skills/notebooklm-studio如果你使用 Claude Code,常見的本地 Skills 目錄是:
mkdir -p ~/.claude/skills
cp -R notebooklm-studio-Skill ~/.claude/skills/notebooklm-studio如果你使用的是其他 Agent,請把資料夾放到該工具指定的 Skills 目錄。
---
快速開始
安裝完成後,在 Codex 裡輸入:
Initiate /notebookLM studio或用中文:
初始化 /notebookLM studioAgent 會根據 Skill 的指引檢查環境、確認 NotebookLM 是否可用,並引導你完成 Google 登入或授權流程。登入與安全驗證需要由使用者本人完成。
---
啟動 Dashboard 操作面板
你可以對 Agent 說:
Launch the NotebookLM Studio dashboard.或中文:
幫我啟動 Dashboard 操作面板。啟動後,Agent 會提供一個本地網址,例如:
http://localhost:8765/如果預設 port 已被使用,會改用下一個可用 port,並告訴你實際網址。
---
常用提示詞
建立研究筆記本
使用 /notebookLM studio 建立一個新的 NotebookLM 筆記本,名稱叫「AI Agent Research Pack」,把以下 URL 加入來源,生成 Report 和 Mind Map,下載結果後幫我分析。把來源變成學習包
使用 /notebookLM studio 把這些來源變成學習包:Report、Quiz、Flashcards、Mind Map。完成後下載所有檔案並幫我整理重點。生成簡報
用 /notebookLM studio 從這個 NotebookLM 筆記本生成 Slide Deck,如果可以就下載 PDF 和 PPTX,然後幫我整理簡報大綱。生成 Audio Overview
用 /notebookLM studio 幫這個筆記本生成 Audio Overview,下載後告訴我內容涵蓋哪些主題。生成 Mind Map 並轉成 HTML
幫我生成 NotebookLM Mind Map,下載 JSON,並轉成互動式 HTML,最後給我兩個檔案連結。搜尋資料並建立 NotebookLM
幫我搜尋 OpenAI Codex Agent 最可靠的教學來源,優先官方文件和高品質教學,建立新的 NotebookLM 筆記本,加入來源,然後用 NotebookLM 生成一份深入教學報告。---
支援的 NotebookLM Studio 內容
| 類型 | 用途 |
|---|---|
| Report | 簡報、study guide、blog post、custom report |
| Mind Map | 概念圖,可轉互動 HTML |
| Audio Overview | Podcast-style audio summary |
| Video Overview | NotebookLM video summary |
| Slide Deck | 簡報 deck |
| Infographic | 圖像摘要 |
| Quiz | 測驗題 |
| Flashcards | 學習卡 |
| Data Table | 結構化表格 |
---
支援的來源
這個 Skill 會優先使用 NotebookLM 官方支援的來源格式,例如 PDF、TXT、Markdown、DOCX、CSV、PPTX、EPUB、音訊、影片和圖片。
它的主要角色是補上來源整理、匯入檢查、生成任務追蹤、結果下載、handoff 和後續 Codex 分析流程。
---
適合的使用場景
研究助理
找來源 → 建 NotebookLM → 生成 Report → Codex 做深度分析學習教練
課程資料 → Study Guide → Quiz → Flashcards → Codex 幫你設計練習內容工廠
來源資料 → NotebookLM Report / Mind Map → Codex 改寫成文章、短片腳本、簡報團隊知識庫
Docs / PR / Meeting notes → NotebookLM → Data Table / Summary / Action Plan---
相容性
這個 Skill 目前以 Codex 工作流為主要設計目標,尤其適合需要本地檔案讀寫、Dashboard 操作、handoff 分析和多步驟研究流程的使用方式。
其他支援 Skills 或本地工具調用的 Agent 可以參考這個專案的概念,但實際使用時可能需要按各自平台調整。
---
安全與使用界線
請只在你有權使用的資料、帳戶和內容上使用本專案。
- 不要用來繞過 paywall、DRM、登入牆、私人存取控制或內容限制
- 不要把私人登入資料、session、token 或本地輸出中的敏感資料提交到 GitHub
- Google 登入與安全驗證應由使用者本人完成
- NotebookLM 是 Google 產品,本專案與 Google 無從屬、合作或背書關係
---
License
This project is licensed under the MIT License.
Artifact Strategy
The skill should produce the user's requested deliverables with the highest-fidelity path available.
Priority Order
1. Official NotebookLM artifact generated by NotebookLM and downloaded locally. 2. Official NotebookLM report/chat/note used as the grounded content layer, then transformed locally by Codex. 3. Pure Codex-derived artifact from the uploaded sources when NotebookLM automation is unavailable, clearly labeled as not generated by NotebookLM.
Multi-Artifact Pipeline
For requests that include more than one output, use scripts/artifact_pipeline.py instead of waiting serially for each artifact. The intended flow is:
1. Prepare and verify sources. 2. Create or reuse the notebook. 3. Submit all requested artifact jobs quickly. Use --no-wait for audio, video, slide deck, report, quiz, flashcards, infographic, and data table. 4. Poll notebooklm artifact list --type all --json as a shared status table. 5. Download each completed artifact immediately. 6. Continue watching other artifacts until they complete, fail, or timeout. 7. Apply local post-processing, such as converting mind-map JSON to HTML.
This prevents slow video generation from blocking successful audio, mind-map, report, or slide-deck delivery.
Recommended command:
python scripts/artifact_pipeline.py run \
--notebook-title "Research Pack" \
--source ./source.pdf \
--artifact audio \
--artifact video \
--artifact mind-map \
--artifact slide-deck \
--download \
--download-slide-format both \
--status-file ./notebooklm_outputs/research-pack-jobs.json \
--out-dir ./notebooklm_outputsThe status file is part of the user experience. If a run is interrupted, inspect it before deciding whether to retry, download already completed artifacts, or launch a narrower fallback job.
Mapping Requests To Artifacts
| User asks for | First choice | Fallback |
|---|---|---|
| Podcast, voice summary, audio brief | generate audio + download audio | NotebookLM report -> local narration script |
| Video summary | generate video or cinematic-video + download video | Slide deck + narration script or local video tool |
| Report, briefing, study guide, FAQ | generate report --format ... + download report | ask --json or ask --save-as-note, then local Markdown/Doc |
| Mind map | generate mind-map + download mind-map | Ask for hierarchical outline JSON, render locally |
| Slides, deck, presentation | generate slide-deck + download slide-deck --format pptx/pdf | Report outline -> local PPTX/HTML deck |
| Infographic | generate infographic + download infographic | Data/report -> local visual artifact |
| Tables, CSV, spreadsheet | generate data-table + download data-table | Ask for source-grounded table JSON/CSV |
| Quiz | generate quiz + download quiz --format json/markdown/html | Ask for quiz schema with answers/citations |
| Flashcards | generate flashcards + download flashcards --format json/markdown/html | Ask for front/back cards with citations |
| Deep analysis JSON | ask --json with schema; optionally save as note | Report + local JSON extraction |
Prompting Pattern
Use concise prompts that specify:
- Audience and expertise level.
- Output language.
- Exact focus and exclusions.
- Desired structure.
- Citation expectations for text outputs.
- Style only when the artifact type supports style.
Example:
Create a source-grounded executive briefing for founders. Focus on market timing, risks, contrarian insight, and next actions. Avoid generic AI hype. Include concise citations where supported.Verification Checklist
Before telling the user the work is done:
notebooklm source list --jsonor equivalent confirms sources were added.- Generation command completed or artifact appears in
artifact list. - Downloaded files exist and have non-zero size.
- Mind-map JSON has been converted to interactive HTML when the user needs a human-readable mind map.
- Text outputs were skimmed for obvious source mismatch or hallucinated unsupported claims.
- The final answer separates official NotebookLM artifacts from Codex-derived artifacts.
NotebookLM Capabilities Reference
Last checked: 2026-05-15.
What NotebookLM Is For
NotebookLM is a source-grounded research assistant. It uploads or imports sources into a notebook, answers questions against those sources with citations, and creates Studio artifacts from the notebook.
The important design point for Codex: NotebookLM is not a generic model endpoint. It is strongest when the user provides a bounded source set and wants grounded outputs from that set.
Supported Source Types
Officially documented source types include:
- PDFs, TXT, Markdown, DOCX, CSV, PPTX, ePub.
- Google Docs, Google Slides, and Google Sheets.
- Web URLs.
- Public YouTube URLs with captions.
- Copy/pasted text.
- Audio files such as MP3, WAV, M4A, MP4, OGG, and other supported audio containers.
- Images such as JPG, PNG, WEBP, GIF, HEIC, TIFF, and related formats.
Important limits and behavior:
- Standard notebooks support up to 50 sources. Paid tiers raise this limit.
- Each source can contain up to 500,000 words or an uploaded file up to 200 MB.
- Web URL import uses page text. Images, embedded videos, nested pages, and paywalled pages are not imported as rich visual context.
- YouTube import uses the transcript/captions only. Private videos, videos without speech, videos without captions, unsafe videos, and very new videos may fail.
- Imported Drive files become static copies. Later edits usually need manual sync or reimport.
- NotebookLM may refuse or partially answer if sources are unsafe, unclear, too short, inaccessible, or do not contain the requested information.
Official Studio Outputs
Current NotebookLM Studio outputs include:
| Output | Use | Typical export/download |
|---|---|---|
| Audio Overview | Podcast-style or brief narrated summary | Audio/video container, often .mp4 through CLI downloads |
| Video Overview | Narrated visual summary | .mp4 |
| Cinematic Video Overview | Richer video style, account/age/language limited | .mp4 |
| Reports | Briefing document, study guide, FAQ-style/custom reports | Docs export in UI; Markdown through CLI |
| Mind Map | Branching visual topic map | Downloadable map; JSON through CLI |
| Flashcards | Study cards with progress UI | CSV in UI; JSON/Markdown/HTML through CLI |
| Quizzes | Interactive quiz questions | JSON/Markdown/HTML through CLI |
| Infographic | Single visual summary | .png |
| Slide Deck | Presentation-ready deck | PDF and PowerPoint |
| Data Table | Structured extraction/comparison table | Google Sheets in UI; CSV through CLI |
Feature availability can vary by account age, Google account type, mobile vs desktop, region, plan, and release rollout. Infographics, slide decks, cinematic video, and some high-limit usage may require age-gated or paid access.
Limits To Mention When Relevant
NotebookLM Standard has lower daily generation limits than Plus, Pro, Ultra, Workspace, or Cloud/Enterprise plans. At the time of this check, Google documents Standard limits such as 100 notebooks/user, 50 sources/notebook, 50 chats/day, 3 audio generations/day, 3 video generations/day, 10 reports/day, 10 quizzes/day, 10 flashcards/day, and 10 mind maps/day. Paid tiers raise limits substantially.
Do not hardcode these limits into user promises. Treat them as subject to change and verify if a workflow depends on them.
Accuracy And Privacy Notes
- NotebookLM artifacts are AI-generated and can contain factual, visual, or audio inaccuracies.
- Audio and video can contain glitches.
- Google states that NotebookLM answers are based on uploaded sources, but the user should still verify important claims.
- For consumer accounts, user data handling differs from Workspace/Enterprise terms. Avoid uploading confidential material unless the user confirms the account and policy are appropriate.
Source URLs Checked
- https://support.google.com/notebooklm/answer/16164461
- https://support.google.com/notebooklm/answer/16215270
- https://support.google.com/notebooklm/answer/16206563
- https://support.google.com/notebooklm/answer/16212820
- https://support.google.com/notebooklm/answer/16454555
- https://support.google.com/notebooklm/answer/16212283
- https://support.google.com/notebooklm/answer/16958963
- https://support.google.com/notebooklm/answer/16758265
- https://support.google.com/notebooklm/answer/16757456
- https://support.google.com/notebooklm/answer/16213268
- https://blog.google/innovation-and-ai/models-and-research/google-labs/notebooklm-data-tables/
- https://blog.google/innovation-and-ai/models-and-research/google-labs/8-ways-to-make-the-most-out-of-slide-decks-in-notebooklm/
notebooklm CLI Reference For Codex
The notebooklm command is provided by the community package notebooklm-py. It is useful for Codex because it exposes NotebookLM workflows through a terminal, but it is not an official Google API. It can break when Google changes internal endpoints, and it needs a real Google session.
Install And Authenticate
Ask the user before installing or opening a browser login.
pip install "notebooklm-py[browser]"
playwright install chromium
notebooklm login
notebooklm auth check --test --jsonCookie import may also be available:
pip install "notebooklm-py[cookies]"
notebooklm login --browser-cookies chromeFor multiple Google accounts, use profiles:
notebooklm profile create work
notebooklm -p work login
notebooklm profile switch workUseful environment variables:
NOTEBOOKLM_HOME: base config directory, default~/.notebooklm.NOTEBOOKLM_PROFILE: active profile name.NOTEBOOKLM_NOTEBOOK: default notebook id.NOTEBOOKLM_AUTH_JSON: inline auth JSON for CI-style environments.NOTEBOOKLM_HL: output language default.
Notebook Commands
notebooklm list --json
notebooklm create "Research Title" --use --json
notebooklm use NOTEBOOK_ID --json
notebooklm status --json
notebooklm summarycreate --use is preferred because later source, generate, and download commands can use the active notebook context.
Source Commands
notebooklm source list --json
notebooklm source add "https://example.com/article" --json
notebooklm source add "./paper.pdf" --json
notebooklm source add - --title "Pasted notes" --json
notebooklm source add-drive FILE_ID "Quarterly deck" --mime-type google-slides --json
notebooklm source wait SOURCE_ID --timeout 300 --json
notebooklm source clean --dry-run --jsonUse source wait when the add response returns a source id and the source needs ingestion time.
Chat And Grounded Extraction
notebooklm ask "What are the key findings? Cite sources." --json
notebooklm ask --prompt-file prompt.txt --json --timeout 120
notebooklm ask "Create a JSON brief with risks, opportunities, and citations." --save-as-note --note-title "Deep Analysis"Use ask --json for source-grounded custom deliverables that are not direct Studio artifacts.
Generate Commands
Uniform flags for most artifact types:
--waitto block until generation finishes.--interval SECONDSfor polling cadence.--retry Nto retry on rate limits.--jsonfor machine-readable results.--language LANGon language-aware artifacts.--prompt-file PATHfor long prompts.
Examples:
notebooklm generate audio "Focus on business impact" --format deep-dive --length default --wait --json
notebooklm generate video "Explain this to executives" --format explainer --style whiteboard --no-wait --json
notebooklm generate cinematic-video "Make it documentary style" --no-wait --json
notebooklm generate report "Create a board briefing" --format briefing-doc --wait --json
notebooklm generate report "Use this custom outline" --format custom --wait --json
notebooklm generate mind-map --instructions "Center on strategic themes" --json
notebooklm generate quiz "Exam prep" --difficulty hard --quantity standard --wait --json
notebooklm generate flashcards "Key terms" --difficulty medium --quantity more --wait --json
notebooklm generate infographic "Use a professional style and highlight 3 metrics" --orientation landscape --detail detailed --style professional --wait --json
notebooklm generate slide-deck "Presenter deck for a 10-minute talk" --format presenter --length default --wait --json
notebooklm generate data-table "Compare vendors by pricing, risks, and differentiators" --wait --jsonDownload Commands
notebooklm download audio ./audio-overview.mp4 --latest --json
notebooklm download video ./video-overview.mp4 --latest --json
notebooklm download slide-deck ./deck.pdf --format pdf --latest --json
notebooklm download slide-deck ./deck.pptx --format pptx --latest --json
notebooklm download infographic ./infographic.png --latest --json
notebooklm download report ./report.md --latest --json
notebooklm download mind-map ./mind-map.json --latest --json
notebooklm download data-table ./table.csv --latest --json
notebooklm download quiz ./quiz.json --format json --latest --json
notebooklm download flashcards ./flashcards.md --format markdown --latest --jsonIf a download command fails because the CLI or NotebookLM rollout changed, list artifacts with:
notebooklm artifact list --type all --json
notebooklm artifact wait ARTIFACT_ID --timeout 1200 --jsonThen download by artifact id if supported.
For long-running multi-artifact jobs, prefer scripts/artifact_pipeline.py. It submits generation with --no-wait where supported, polls artifact list, downloads completed artifacts, and converts mind maps to HTML.
Troubleshooting
Run these before blaming NotebookLM:
notebooklm --version
notebooklm doctor --json
notebooklm auth check --test --json
notebooklm status --paths
notebooklm list --limit 5 --jsonCommon causes:
- Google session expired: run
notebooklm loginagain. - Account does not have the feature or quota: generate fewer artifacts, wait for quota reset, or use a supported account.
- Source import failed: check access, captions, file size, supported type, and safety flags.
- CLI/API drift: use the Browser or Chrome plugin to operate NotebookLM UI manually, then update this skill.
NotebookLM Studio Quickstart Guide
Use this guide after first-time setup or whenever the user asks how to use the skill.
What The First Experience Should Feel Like
A new user should not need to understand notebooklm-py, Playwright, Google cookies, CLI profiles, or output flags. They should be able to say something like:
用 NotebookLM Studio 把這些資料做成 podcast、slide deck、mind map 和表格。Codex should then:
1. Check whether the NotebookLM CLI exists. 2. Install the CLI and browser support if the user approves. 3. Start Google login when needed. 4. Wait for the user to complete login manually. 5. Verify authentication. 6. Classify sources and warn about risks. 7. Create or reuse a notebook. 8. Submit requested official NotebookLM artifacts as parallel jobs where possible. 9. Poll the shared artifact status table. 10. Download each completed file immediately. 11. Convert mind-map JSON into interactive HTML. 12. Explain which outputs came from NotebookLM and which were derived by Codex. 13. Optionally launch the local dashboard so the user can browse notebooks, run workflows, and hand outputs back to Codex.
One-Time Setup Commands
Environment check:
python ~/.codex/skills/notebooklm-studio/scripts/bootstrap_notebooklm.py --json --print-guideInstall dependencies after user approval:
python ~/.codex/skills/notebooklm-studio/scripts/bootstrap_notebooklm.py --install --print-guideStart Google login and verify auth:
python ~/.codex/skills/notebooklm-studio/scripts/bootstrap_notebooklm.py --login --auth-test --print-guideThe user must manually finish Google login in the browser. Codex can continue after the terminal command returns.
Launch The Local Dashboard
After setup and login, start the visual control room from the user's active project folder:
python ~/.codex/skills/notebooklm-studio/scripts/dashboard_server.py \
--host 127.0.0.1 \
--port 8765 \
--profile default \
--out-dir ./notebooklm_outputs/dashboardOpen:
http://127.0.0.1:8765/The dashboard lets the user browse NotebookLM notebooks, inspect sources and official artifacts, run one-click workflow recipes, run native Studio tools, watch job status, download outputs, and show a Codex handoff prompt for completed dashboard jobs.
If 8765 is occupied, use another local port:
python ~/.codex/skills/notebooklm-studio/scripts/dashboard_server.py \
--host 127.0.0.1 \
--port 8766 \
--profile default \
--out-dir ./notebooklm_outputs/dashboardKeep the dashboard bound to 127.0.0.1 unless the user explicitly asks for network access and understands the risk.
Normal Use Through Codex
The user can speak naturally:
請把 /path/to/report.pdf 和這個網址做成 Audio Overview、Mind Map、Slide Deck、Infographic 和 Data Table,輸出繁體中文。Codex should translate that into a manifest, plan, run, and verification sequence.
Useful Direct Commands
Build a source manifest:
python ~/.codex/skills/notebooklm-studio/scripts/source_manifest.py \
/path/to/report.pdf \
https://example.com/article \
https://www.youtube.com/watch?v=VIDEO_IDDry-run a full NotebookLM workflow:
python ~/.codex/skills/notebooklm-studio/scripts/artifact_pipeline.py plan \
--notebook-title "Market Research" \
--source /path/to/report.pdf \
--source https://example.com/article \
--artifact audio \
--artifact report \
--artifact mind-map \
--artifact slide-deck \
--artifact infographic \
--artifact data-table \
--download \
--download-slide-format both \
--out-dir ./notebooklm_outputsRun it for real after authentication is ready:
python ~/.codex/skills/notebooklm-studio/scripts/artifact_pipeline.py run \
--notebook-title "Market Research" \
--source /path/to/report.pdf \
--source https://example.com/article \
--artifact audio \
--artifact report \
--artifact mind-map \
--artifact slide-deck \
--artifact infographic \
--artifact data-table \
--download \
--download-slide-format both \
--status-file ./notebooklm_outputs/market-research-jobs.json \
--out-dir ./notebooklm_outputsConvert an existing NotebookLM mind-map JSON to an interactive HTML view:
python ~/.codex/skills/notebooklm-studio/scripts/mindmap_html.py \
./notebooklm_outputs/mind-map.json \
-o ./notebooklm_outputs/mind-map.htmlRun the dashboard helper smoke test:
python ~/.codex/skills/notebooklm-studio/scripts/dashboard_smoke_test.pyExamples
1. A founder briefing from PDFs and URLs
用 NotebookLM Studio 讀這三份 PDF 和兩個競品網站,生成一份 board briefing、data table、slide deck 和 podcast。語言用繁體中文,觀點要尖銳,不要泛泛而談。Best artifact plan: report, data-table, slide-deck, audio.
2. A learning pack from a YouTube lecture
把這個 YouTube 課程做成 study guide、quiz、flashcards 和 mind map,幫我準備考試。Best artifact plan: report --report-format study-guide, quiz, flashcards, mind-map.
3. A visual explainer package
把這份研究報告變成 Video Overview、Infographic 和 Presenter Slides,讓普通人 5 分鐘內明白。Best artifact plan: video, infographic, slide-deck.
4. Deep analysis JSON
根據這些來源輸出 deep-analysis JSON,包含 thesis、evidence、counterarguments、risks、opportunities、citations。Best path: use NotebookLM ask --json as the grounded layer. This is a derived Codex deliverable unless NotebookLM adds a native deep-analysis JSON artifact.
Practical Tips
- Good sources matter more than fancy prompts. A clean, bounded source set produces better artifacts.
- NotebookLM imports YouTube transcripts, not the full video visuals.
- Web URLs import text, not every embedded image or nested page.
- Video and slide artifacts can take several minutes or longer.
- Multi-artifact runs should use the pipeline so a failed or slow video does not block the other outputs.
- Mind maps should be returned as both official JSON and interactive HTML for easier reading.
- Some outputs depend on account plan, age-gated access, region, and rollout status.
- Always separate official NotebookLM artifacts from Codex-derived artifacts in the final answer.
- For important business, legal, medical, or financial content, verify citations and source coverage before sharing.
Source Ingestion Strategy
NotebookLM accepts many source types, but "anything" still needs staging. Codex should normalize the user's inputs before upload and warn early when NotebookLM is likely to reject something.
Safe Source Policy
Proceed when the source is:
- Uploaded by the user.
- A public URL that is not paywalled.
- A public YouTube URL with captions.
- A local file in the workspace or a path the user explicitly provided.
- A Google Drive source the user can access through their signed-in NotebookLM account.
Do not proceed when the request requires bypassing access controls, scraping paid content, or uploading sensitive/confidential data without explicit confirmation.
Preparation Pattern
1. Collect source strings exactly as provided. 2. Run scripts/source_manifest.py to classify and flag them. 3. For missing local files, ask for the correct path or upload. 4. For unsupported local formats, convert only when the conversion is safe and loss is acceptable:
- HTML/email/chat exports -> Markdown or TXT.
- Spreadsheets -> CSV when Sheets import is unavailable.
- Local video -> transcript/audio extraction for speech; note that visual details are not preserved unless separately described.
- Images -> upload directly if NotebookLM supports the extension; for dense diagrams, add a Codex-authored OCR/description note if needed.
5. For many sources, group by theme and create multiple notebooks if the source count or token budget will reduce answer quality.
Upload Priorities
Prefer original rich sources when NotebookLM supports them:
1. Native PDF, PPTX, DOCX, CSV, MD, TXT, ePub. 2. Google Docs/Slides/Sheets through Drive import. 3. Web URL when page text is enough. 4. YouTube URL when captions are available. 5. Pasted text or normalized Markdown when the original format is not reliable.
Quality Checks After Upload
After adding sources:
notebooklm source list --json
notebooklm summaryFor high-value sources, ask a sanity question:
notebooklm ask "List the sources you can see, their main topics, and any import problems." --jsonIf the answer suggests missing content, stop and repair the source set before generating polished artifacts.
#!/usr/bin/env python3
"""Automate first-time setup for the NotebookLM Studio skill."""
from __future__ import annotations
import argparse
import importlib.metadata
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
QUICKSTART = ROOT / "references" / "quickstart-guide.md"
DASHBOARD_SERVER = ROOT / "scripts" / "dashboard_server.py"
def run_command(args: list[str], timeout: int | None = None) -> dict[str, Any]:
try:
proc = subprocess.run(args, capture_output=True, text=True, timeout=timeout, check=False)
except FileNotFoundError:
return {"ok": False, "returncode": 127, "stdout": "", "stderr": "command not found", "command": args}
except subprocess.TimeoutExpired as exc:
return {
"ok": False,
"returncode": None,
"stdout": exc.stdout or "",
"stderr": f"timed out after {timeout}s",
"command": args,
}
return {
"ok": proc.returncode == 0,
"returncode": proc.returncode,
"stdout": proc.stdout.strip(),
"stderr": proc.stderr.strip(),
"command": args,
}
def package_version(name: str) -> str | None:
try:
return importlib.metadata.version(name)
except importlib.metadata.PackageNotFoundError:
return None
def auth_material(profile: str) -> dict[str, Any]:
home = Path(os.environ.get("NOTEBOOKLM_HOME", Path.home() / ".notebooklm")).expanduser()
paths = [home / "profiles" / profile / "storage_state.json", home / "storage_state.json"]
existing = [str(path) for path in paths if path.exists()]
return {
"home": str(home),
"profile": profile,
"storage_files_found": existing,
"has_inline_auth_json": bool(os.environ.get("NOTEBOOKLM_AUTH_JSON")),
"has_any_auth_material": bool(existing) or bool(os.environ.get("NOTEBOOKLM_AUTH_JSON")),
}
def status(profile: str, auth_test: bool = False) -> dict[str, Any]:
cli = shutil.which("notebooklm")
data: dict[str, Any] = {
"notebooklm_cli": cli,
"notebooklm_py_version": package_version("notebooklm-py"),
"playwright_version": package_version("playwright"),
"auth": auth_material(profile),
"checks": {},
}
if cli:
data["checks"]["version"] = run_command(["notebooklm", "--version"], timeout=20)
if auth_test:
data["checks"]["auth_test"] = run_command(
["notebooklm", "-p", profile, "auth", "check", "--test", "--json"],
timeout=90,
)
ready = bool(cli) and data["auth"]["has_any_auth_material"]
if auth_test:
ready = ready and bool(data["checks"].get("auth_test", {}).get("ok"))
data["ready_for_notebooklm"] = ready
data["needs"] = []
if not cli:
data["needs"].append("install notebooklm-py with browser support")
if not data["auth"]["has_any_auth_material"]:
data["needs"].append("manual Google login")
if auth_test and not data["checks"].get("auth_test", {}).get("ok"):
data["needs"].append("working NotebookLM auth test")
return data
def install_dependencies() -> list[dict[str, Any]]:
steps = []
steps.append(run_command([sys.executable, "-m", "pip", "install", "notebooklm-py[browser]"], timeout=None))
if steps[-1]["ok"]:
steps.append(run_command([sys.executable, "-m", "playwright", "install", "chromium"], timeout=None))
return steps
def login(profile: str, browser: str) -> dict[str, Any]:
cmd = ["notebooklm", "-p", profile, "login"]
if browser:
cmd.extend(["--browser", browser])
return run_command(cmd, timeout=None)
def guide_text(state: dict[str, Any]) -> str:
readiness = "ready" if state.get("ready_for_notebooklm") else "not ready yet"
needs = state.get("needs") or []
quickstart = QUICKSTART.read_text(encoding="utf-8") if QUICKSTART.exists() else ""
preface = f"""# NotebookLM Studio Setup Result\n\nCurrent status: **{readiness}**.\n\nDetected CLI: `{state.get('notebooklm_cli') or 'missing'}`.\nDetected notebooklm-py: `{state.get('notebooklm_py_version') or 'not installed'}`.\nDetected Playwright: `{state.get('playwright_version') or 'not installed'}`.\nAuth material: `{'yes' if state.get('auth', {}).get('has_any_auth_material') else 'no'}`.\n\n"""
if needs:
preface += "Next required steps:\n" + "".join(f"- {need}\n" for need in needs) + "\n"
else:
preface += "NotebookLM automation is ready. You can now ask Codex to create notebooks and artifacts.\n\n"
if DASHBOARD_SERVER.exists():
preface += (
"Local dashboard command:\n"
f"`python {DASHBOARD_SERVER} --host 127.0.0.1 --port 8765 --profile "
f"{state.get('auth', {}).get('profile') or 'default'} --out-dir ./notebooklm_outputs/dashboard`\n\n"
)
return preface + quickstart
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--profile", default=os.environ.get("NOTEBOOKLM_PROFILE", "default"))
parser.add_argument("--install", action="store_true", help="install notebooklm-py[browser] and Playwright Chromium")
parser.add_argument("--login", action="store_true", help="start manual Google login")
parser.add_argument("--browser", default="chromium", choices=["chromium", "msedge"])
parser.add_argument("--auth-test", action="store_true", help="run NotebookLM auth check after setup")
parser.add_argument("--json", action="store_true", help="print JSON state")
parser.add_argument("--print-guide", action="store_true", help="print a user guide after setup")
parser.add_argument("--output-guide", help="write the generated guide to a Markdown file")
args = parser.parse_args()
actions: list[dict[str, Any]] = []
before = status(args.profile, auth_test=False)
if args.install and not before.get("notebooklm_cli"):
actions.extend(install_dependencies())
mid = status(args.profile, auth_test=False)
if args.login:
if not mid.get("notebooklm_cli"):
actions.append({"ok": False, "returncode": 127, "stderr": "notebooklm CLI is missing; install first", "command": ["notebooklm", "login"]})
else:
actions.append(login(args.profile, args.browser))
final = status(args.profile, auth_test=args.auth_test)
final["actions"] = actions
if args.output_guide:
Path(args.output_guide).write_text(guide_text(final), encoding="utf-8")
final["guide_path"] = str(Path(args.output_guide).resolve())
if args.json:
print(json.dumps(final, indent=2, sort_keys=True))
if args.print_guide:
if args.json:
print("\n--- GUIDE ---\n")
print(guide_text(final))
if not args.json and not args.print_guide:
print(f"NotebookLM Studio is {'ready' if final['ready_for_notebooklm'] else 'not ready yet'}")
for need in final["needs"]:
print(f"- {need}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
"""Offline smoke tests for the NotebookLM Studio dashboard helper."""
from __future__ import annotations
import importlib.util
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def load_dashboard_module():
spec = importlib.util.spec_from_file_location("dashboard_server", ROOT / "scripts" / "dashboard_server.py")
if spec is None or spec.loader is None:
raise AssertionError("Unable to load dashboard server module")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_dashboard_assets_exist() -> None:
assert (ROOT / "dashboard" / "index.html").exists()
assert (ROOT / "dashboard" / "app.js").exists()
assert (ROOT / "dashboard" / "styles.css").exists()
assert (ROOT / "dashboard" / "assets" / "NotebookLM_logo.png").exists()
app_js = (ROOT / "dashboard" / "app.js").read_text(encoding="utf-8")
assert "Show Prompt for Codex" in app_js
assert "data:image" not in app_js
def test_prepare_agent_prompt_writes_text_prompt() -> None:
dashboard = load_dashboard_module()
with tempfile.TemporaryDirectory() as tmp:
dashboard.OUTPUT_ROOT = Path(tmp) / "dashboard"
dashboard.JOBS.clear()
dashboard.CACHE.clear()
dashboard.PERSISTED_JOBS_LOADED = True
job = {
"id": "job-test",
"notebook_id": "nb-test",
"notebook_title": "Taiwan Banking AI Visibility Analysis",
"label": "Executive Briefing Suite",
"purpose": "Analyze banking AI visibility outputs",
"kind": "recipe",
"artifacts": ["Report"],
"commands": ["report"],
"status": "completed",
"created_at": "2026-05-16T00:00:00",
"updated_at": "2026-05-16T00:01:00",
"outputs": [
{
"command": "report",
"label": "Report",
"status": "completed",
"artifact_id": "artifact-test",
"download_status": "downloaded",
"downloaded_files": [
{
"path": "/tmp/taiwan-banking-report.md",
"size": 1234,
"format": "md",
}
],
}
],
}
dashboard.JOBS.append(job)
result = dashboard.prepare_agent_prompt("job-test")
assert result["ok"] is True
assert result["prompt"].strip().startswith("Analyze the following NotebookLM generated outputs")
assert "data:image" not in result["prompt"]
assert result["prompt_path"].endswith("latest_agent_prompt.md")
prompt_path = Path(result["prompt_path"])
assert prompt_path.exists()
assert prompt_path.read_text(encoding="utf-8").strip() == result["prompt"].strip()
def test_prepare_agent_prompt_requires_downloaded_files() -> None:
dashboard = load_dashboard_module()
with tempfile.TemporaryDirectory() as tmp:
dashboard.OUTPUT_ROOT = Path(tmp) / "dashboard"
dashboard.JOBS.clear()
dashboard.CACHE.clear()
dashboard.PERSISTED_JOBS_LOADED = True
dashboard.JOBS.append(
{
"id": "job-empty",
"notebook_id": "nb-test",
"notebook_title": "No Downloads",
"status": "completed",
"outputs": [{"label": "Report", "status": "completed", "downloaded_files": []}],
}
)
result = dashboard.prepare_agent_prompt("job-empty")
assert result["ok"] is False
assert result["error"] == "No downloaded files are ready yet"
def main() -> int:
test_dashboard_assets_exist()
test_prepare_agent_prompt_writes_text_prompt()
test_prepare_agent_prompt_requires_downloaded_files()
print("dashboard smoke tests passed")
return 0
if __name__ == "__main__":
raise SystemExit(main())