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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[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.