DeepBotDownload

Notes board

A card and a page: files the Bot writes, storage for pins, ask and openPage.

app.json

{
  "format": "deepbot.app/v1",
  "id": "notes-board",
  "name": "Notes board",
  "version": "1.0.0",
  "tier": "free",
  "vendor": "DeepBot examples",
  "summary": "Notes your Bot writes for you, shown in the chat and kept on a board of their own.",
  "description": "When you ask your Bot to note something down, it writes the note as a Markdown file and shows it in the chat. All notes live on the Notes board page: pin the important ones, read, delete, or ask the Bot for a new one.",
  "icon": "ui/icon.svg",
  "permissions": {
    "files": ["notes"],
    "ask": true
  },
  "skill": "skill/SKILL.md",
  "cards": [
    {
      "name": "showNote",
      "title": "Note",
      "entry": "ui/card-note.html",
      "height": 220,
      "description": "Show one note in the chat after writing it to notes/<slug>.md. Pass `path` (e.g. notes/shopping-list.md).",
      "confirmation": "The note is on screen. Do not repeat its text; at most one short sentence.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "The note's file, notes/<slug>.md" }
        },
        "required": ["path"]
      }
    }
  ],
  "pages": [
    { "id": "board", "title": "Notes board", "entry": "ui/board.html" }
  ]
}

skill/SKILL.md

---
name: notes-board
description: "Use when the person asks to note, jot down, remember on paper, or save a short text (a list, an idea, meeting points, a recipe): writes it as a note in the Notes board app and shows it in the chat."
---

# Notes board

The Notes board app keeps notes as Markdown files in the workspace, one file per note:

```
notes/<slug>.md
```

`<slug>` is the title in lowercase with dashes (`Shopping list` -> `shopping-list`). If that file
already exists and the person did not ask to change it, add `-2`, `-3` ...

## Writing a note

1. Write the file with the title as the first line, then the text:

   ```markdown
   # Shopping list

   - milk
   - eggs
   ```

   Keep the person's own words and language. Lists stay lists.

2. Show it: call `showNote` with `path` set to the file you wrote. Do not repeat the note's text in
   your answer; the card shows it.

## Changing a note

Read the file, change it, write it back, and show it again with `showNote`.

## The board

The person can open all notes on the Notes board page (Apps -> Notes board). Pinned notes are the
person's own choice; you do not see them.

ui/board.html

<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><link rel="stylesheet" href="style.css"></head>
<body class="db-page">
  <div class="head">
    <div>
      <h1 class="db-h1">Notes board</h1>
      <div class="db-muted" id="count">…</div>
    </div>
    <div class="db-row">
      <button class="db-btn ghost" id="refresh">Refresh</button>
      <button class="db-btn" id="new">New note with the Bot</button>
    </div>
  </div>
  <div id="view"></div>
  <script type="module" src="board.js"></script>
</body>
</html>

ui/board.js

/*
 * THE BOARD: a full page in DeepBot (Apps -> Notes board). Lists every note in notes/, pinned
 * first. Pins are the person's own and live in the app's storage (deepbot.storage), not in the
 * workspace: the Bot does not see them.
 */
import { esc, listNotes, render } from "./notes.js";

const view = document.getElementById("view");
let notes = [];
let pinned = [];
let open = null; // the note being read, or null for the grid

async function load() {
	pinned = (await deepbot.storage.get("pinned")) || [];
	notes = await listNotes();
	document.getElementById("count").textContent = `${notes.length} ${notes.length === 1 ? "note" : "notes"}`;
	draw();
}

function draw() {
	if (open) {
		const n = notes.find((x) => x.path === open);
		if (n) {
			view.innerHTML = `<div class="db-card paper reader">
				<div class="note-title">${esc(n.title)}</div>
				<div class="note-body">${render(n.body)}</div>
				<div class="acts">
					<button class="db-btn ghost" data-act="back">All notes</button>
					<button class="db-btn ghost" data-act="delete" data-path="${esc(n.path)}">Delete</button>
				</div></div>`;
			return;
		}
		open = null;
	}
	if (!notes.length) {
		view.innerHTML = `<div class="empty db-muted">No notes yet. Ask your Bot: “note down …”.</div>`;
		return;
	}
	const sorted = [...notes].sort((a, b) => pinned.includes(b.path) - pinned.includes(a.path));
	view.innerHTML = `<div class="grid">${sorted
		.map(
			(n) => `<div class="db-card tile" data-open="${esc(n.path)}">
				<button class="pin ${pinned.includes(n.path) ? "on" : ""}" data-pin="${esc(n.path)}" title="Pin">📌</button>
				<div class="note-title">${esc(n.title)}</div>
				<div class="note-body">${render(n.body)}</div>
			</div>`,
		)
		.join("")}</div>`;
}

view.addEventListener("click", async (e) => {
	const pin = e.target.closest("[data-pin]");
	if (pin) {
		const p = pin.dataset.pin;
		pinned = pinned.includes(p) ? pinned.filter((x) => x !== p) : [...pinned, p];
		await deepbot.storage.set("pinned", pinned);
		return draw();
	}
	const act = e.target.closest("[data-act]");
	if (act?.dataset.act === "back") {
		open = null;
		return draw();
	}
	if (act?.dataset.act === "delete") {
		if (act.dataset.sure !== "1") {
			act.dataset.sure = "1";
			act.textContent = "Delete for good?";
			return;
		}
		await deepbot.files.delete(act.dataset.path);
		open = null;
		return load();
	}
	const tile = e.target.closest("[data-open]");
	if (tile) {
		open = tile.dataset.open;
		draw();
	}
});

document.getElementById("refresh").addEventListener("click", load);
// From a page there is no conversation yet: ask() starts a new one with this text.
document.getElementById("new").addEventListener("click", () => deepbot.ask("Write me a new note: "));

await deepbot.ready;
// Opened from a card with openPage("board", { path }): show that note straight away.
open = deepbot.args.path || null;
await load();

ui/card-note.html

<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><link rel="stylesheet" href="style.css"></head>
<body>
  <div class="db-card paper" id="root"><span class="db-muted">Opening the note…</span></div>
  <script type="module" src="card-note.js"></script>
</body>
</html>

ui/card-note.js

/*
 * THE NOTE IN THE CHAT. The Bot calls showNote({ path }); DeepBot opens this page in a card with
 * those arguments in deepbot.args. The card reads the file, shows it, and tells the Bot it worked
 * (deepbot.done) or what went wrong (deepbot.fail), so the Bot never says "here it is" over an
 * empty card.
 */
import { esc, readNote, render } from "./notes.js";

const root = document.getElementById("root");
let note = null;

async function show() {
	const path = String(deepbot.args.path || "");
	try {
		note = await readNote(path);
	} catch {
		root.innerHTML = `<span class="db-muted">There is no note at ${esc(path)}.</span>`;
		deepbot.fail(`No note at ${path}. Write the file first, then call showNote.`);
		return;
	}
	root.innerHTML = `
		<div class="note-title">${esc(note.title)}</div>
		<div class="note-body">${render(note.body)}</div>
		<div class="acts">
			<button class="db-btn ghost" id="longer">Make it longer</button>
			<button class="db-btn" id="board">Open the board</button>
		</div>`;
	deepbot.done();
}

root.addEventListener("click", (e) => {
	if (!note) return;
	// A message to the Bot in this conversation (needs "ask": true in app.json).
	if (e.target.closest("#longer")) deepbot.ask(`Make the note "${note.title}" (${note.path}) longer and more detailed.`);
	// One of the app's own pages, opened on this note: the page gets { path } as deepbot.args.
	if (e.target.closest("#board")) deepbot.openPage("board", { path: note.path });
});

await deepbot.ready; // arguments, light or dark, and DeepBot's colours have arrived
await show();
// While the Bot is still streaming the call, the arguments can change: show the new ones.
deepbot.on("args", show);

ui/icon.svg

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><title>Notes board</title><rect width="64" height="64" rx="16" fill="#FFD43B"/><rect x="15" y="13" width="34" height="40" rx="5" fill="#fff"/><path d="M22 25h20M22 32h20M22 39h13" stroke="#14161C" stroke-width="3" stroke-linecap="round"/><circle cx="46" cy="16" r="5" fill="#E8590C"/></svg>

ui/notes.js

/*
 * What the card and the board share: reading notes and turning their Markdown into safe HTML.
 * Notes are files in the workspace (notes/<slug>.md); the app may touch only that folder
 * (permissions.files in app.json).
 */

export const DIR = "notes";

export const esc = (s) =>
	String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);

/** Split a note into its title (the first "# " line) and the rest. */
export function parseNote(text, path) {
	const lines = String(text).replace(/\r/g, "").split("\n");
	const first = lines.findIndex((l) => l.trim());
	let title = path.split("/").pop().replace(/\.md$/, "").replace(/-/g, " ");
	let body = lines;
	if (first >= 0 && /^#\s+/.test(lines[first])) {
		title = lines[first].replace(/^#\s+/, "").trim();
		body = lines.slice(first + 1);
	}
	return { path, title, body: body.join("\n").trim() };
}

/** A small Markdown subset: paragraphs, - lists, [ ] checkboxes, **bold**, *italic*, `code`. */
export function render(md) {
	const inline = (s) =>
		esc(s)
			.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>")
			.replace(/\*(.+?)\*/g, "<i>$1</i>")
			.replace(/`(.+?)`/g, "<code>$1</code>");
	const out = [];
	let list = null;
	for (const line of md.split("\n")) {
		const item = /^\s*[-*]\s+(\[( |x)\]\s+)?(.*)$/i.exec(line);
		if (item) {
			if (!list) {
				list = [];
				out.push(list);
			}
			const box = item[1] ? `<input type="checkbox" disabled ${item[2].toLowerCase() === "x" ? "checked" : ""}> ` : "";
			list.push(`<li>${box}${inline(item[3])}</li>`);
			continue;
		}
		list = null;
		if (line.trim()) out.push(`<p>${inline(line)}</p>`);
	}
	return out.map((x) => (Array.isArray(x) ? `<ul>${x.join("")}</ul>` : x)).join("");
}

export async function readNote(path) {
	return parseNote(await deepbot.files.read(path, "text"), path);
}

/** All notes, newest first. */
export async function listNotes() {
	const { items } = await deepbot.files.list(DIR);
	const files = items.filter((i) => !i.dir && i.name.endsWith(".md"));
	files.sort((a, b) => b.modified - a.modified);
	return Promise.all(files.map(async (f) => ({ ...(await readNote(f.path)), modified: f.modified })));
}

ui/style.css

/* On top of DeepBot's kit (the host adds it to every page): its colours, so light and dark just work. */
.note-title { font-size: 16px; font-weight: 650; margin: 0 0 6px; }
.note-body p { margin: 0 0 6px; }
.note-body ul { margin: 0 0 6px; padding-left: 20px; }
.note-body li { margin: 2px 0; }
.note-body code { background: var(--muted); padding: 1px 5px; border-radius: 5px; }
.paper { border-left: 4px solid var(--brand); }
.acts { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; margin-top: 16px; }
.tile { position: relative; cursor: pointer; min-height: 120px; }
.tile:hover { border-color: var(--muted-foreground); }
.tile .note-body { max-height: 140px; overflow: hidden; color: var(--muted-foreground); font-size: 13px; }
.pin { position: absolute; top: 8px; right: 8px; border: 0; background: transparent; cursor: pointer; font-size: 15px; opacity: .35; }
.pin.on, .tile:hover .pin { opacity: 1; }
.head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.empty { padding: 40px; text-align: center; border: 1px dashed var(--border); border-radius: var(--radius); margin-top: 16px; }
.reader { margin-top: 16px; }
.note-body li:has(> input[type="checkbox"]) { list-style: none; margin-left: -20px; }
.note-body li > input[type="checkbox"] { margin: 0 6px 0 0; vertical-align: -1px; }

listing.md

---
categories: [productivity]
tags: [notes, lists, example]
screenshots:
  - A note in the chat, written by the Bot
  - All notes on the board, pinned first
---

An example app from the DeepBot docs. Ask your Bot to **note something down** and it writes a note
and shows it right in the chat. Every note is kept on the **Notes board** page, where you can pin,
read and delete them.

Notes are plain Markdown files in your workspace (`notes/`), so they are yours: open them in any editor.

## FAQ

### Can the Bot see my pinned notes?
No. Pins are kept by the app for you only.

## Changelog

### 1.0.0
- First version.