Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 124 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,47 +25,141 @@
</p>

<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/lucid-dark.png" />
<img alt="Lucid: a page of notes with a diagram, the list of changes on the right and the input box at the bottom" src="docs/images/lucid-light.png" width="900" />
</picture>
<img alt="Lucid in a dark app with a light page: a page of notes with a diagram, the list of changes on the right and the input box at the bottom" src="docs/images/readme/page-look-light-page.png" width="900" />
</p>

## Why I made it

I'm the author. For a while I couldn't type comfortably. I wasn't working, but I still wanted to use the time to keep learning,
so I went back to system design and other concepts I wanted to revise. Taking notes was the hard part.
For a while I couldn't type comfortably, so I built Lucid for myself: a productivity and accessibility tool for taking notes and
learning system design by voice, or with one hand.

There are good open-source tools that turn voice into text, and others that draw diagrams. I couldn't find one project where I
could just speak and get something *structured*: sections, a list of options with the rejected ones crossed out, a table, a
diagram. I like minimal themes, so I built the one I wanted, for myself first.
I learn by thinking out loud, and I want to see it as I say it: the diagram when I explain a system, the notes taking shape when I
revise, and a way to cut what I got wrong and say it again. There are good open-source tools that turn voice into text, and others
that draw diagrams, but I couldn't find one project where I could simply speak and get something *structured*, entirely on my own
computer. I like minimal themes, so I built the one I wanted, for myself first.

This is the first version, and it doesn't listen by itself. You dictate with an app that pastes text, like
[Wispr Flow](https://wisprflow.ai), [OpenWhispr](https://github.com/OpenWhispr/openwhispr) or
[VoiceNative](https://github.com/triggeredcode/voicenative) (my own open-source voice-to-text for macOS), and Lucid turns what
lands in the box into the page. A fully local voice pipeline will take me some time to get right; you can start with those.

The hardest part was the drawing: a diagram engine and a table engine good enough to build from speech. Command mode edits the
page by talking to it, and the history keeps what you said, what the model understood and what it changed, so you can always see
why a page looks the way it does. The model runs on your own computer. I hold it to a bar of 90% on my own evaluation
([docs/evals.md](docs/evals.md)), and Edit mode and undo are for the rest.

Where it might go, in no particular order: a faster, more live mode with smaller local models (5B to 8B, maybe Gemma) or with
cloud models through Ollama; fine-tuned models; and fields other than system design. Voice stays in its own project,
VoiceNative, and I may integrate the two without merging them. These are ideas, not promises.

## What you get

- **Speech becomes structure, not a wall of text.** Dictation gives you the words. Lucid gives you the document: an outline of
sections, a list of options with the rejected ones crossed out, a table that compares them, a diagram of how the parts connect.
- **By voice or by keyboard.** Notes, commands (*"cross out Memcached"*, *"add a CDN in front of the web server"*, *"undo"*),
diagrams and tables can all be spoken. See [dictation](docs/dictation.md) for the apps and for using Lucid with one hand.
- **Made for system design, open to other fields.** The note formats are plain data: a concept note, a system design, a
comparison, a blank page, and one more entry for anything else. [See them](docs/formats.md).
- **You stay in charge.** Every change by the model is one undo step, any earlier change can be reverted on its own, and your
words are never lost.
- **Local and private.** No account, no cloud service. The model runs in [LM Studio](https://lmstudio.ai) on your computer.
- **A toolbox for AI agents.** An MCP server lets Claude Code, or a model in LM Studio's chat, read and edit the same pages.
## How it works

```mermaid
flowchart LR
you["You, by voice or keyboard"] --> box["The input box"]
box --> model["A local model in LM Studio<br/>(for example Qwen 3.8)<br/>sees the page and its history"]
model --> page["Blocks on your page:<br/>headings, lists, tables, diagrams"]
page --> history["The history keeps what you said,<br/>what was understood, what changed"]
```

The model runs on your computer and never leaves it. It always has the page in front of it, so it does not only add text: it
puts what you said under the right heading and builds the right kind of block. Every change is exactly one undo step, and your
words are never lost.

## The two parts I like most

### Diagram mode: keywords, connections, editing

A diagram of ten parts is hard to say in one breath, so there is a guided builder with three phases. First the **keywords**: say
the parts of the system. Then the **connections**: say how they connect, one at a time. Then **editing**: everything has a number,
so a fix is one short sentence (*"connection 3 is reads and writes"*, *"rename 3 to Cache"*). Say *"done"* and the diagram goes
onto your page. A database is drawn as a cylinder, a queue as a pipe, a CDN as a cloud, a load balancer as a hexagon.

<table>
<tr>
<td width="33%" align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/readme/builder-keywords-dark.png" />
<img alt="Diagram mode, phase 1: the keywords, the parts of the system, numbered" src="docs/images/readme/builder-keywords.png" />
</picture>
<br /><sub><b>1. Keywords</b></sub>
</td>
<td width="33%" align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/readme/builder-connections-dark.png" />
<img alt="Diagram mode, phase 2: the connections between the parts, numbered" src="docs/images/readme/builder-connections.png" />
</picture>
<br /><sub><b>2. Connections</b></sub>
</td>
<td width="33%" align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/readme/builder-edit-dark.png" />
<img alt="Diagram mode, phase 3: editing by number" src="docs/images/readme/builder-edit.png" />
</picture>
<br /><sub><b>3. Editing</b></sub>
</td>
</tr>
</table>

### Command mode: talk to the page

Because the model has the whole page in context, you can talk about it and it edits the page for you: *"add a cache in front of
the database"*, *"cross out Memcached, we are choosing the local LRU"*, *"add a row for cost to this table"*, *"undo"*. Crossing
out keeps the idea on the page, struck through, so you can still see what you decided against. Hold the right ⌥ key and
everything you say is one command, which you can do with one hand.

## History, edit mode and how well it works

The history keeps what you said, what the model understood and what it changed, so you can always see why a page looks the way
it does, take back any single change (not just the last one), or edit what you said and send it again. When the model gets
something wrong there is **Edit mode**: you just type in the page.

<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/readme/block-numbers-dark.png" />
<img alt="Block numbers in the page margin; block 4, a diagram, lists its numbered parts and connections" src="docs/images/readme/block-numbers.png" width="820" />
</picture>
</p>

I test the model against my own set of cases with the real model ([docs/evals.md](docs/evals.md)) and hold it to a bar of 90%.
In my latest run it got 98 of 99 cases right. To get the most from it: say things in short pieces, use diagram mode for diagrams
and command mode to change the page, and look at the history whenever something surprises you.

## Keep your notes

- **PDF**, typeset on your computer, with real diagrams. Settings → **PDF look** sets the paper, the type, the margins and an
accent colour, and you can edit the template itself.
- **A whole note**, with its history, to back up or move to another computer, and **Markdown**.
- **Three note formats** to start from (a concept note, a system design, a comparison) or a blank page; formats are plain data, so
one of your own is one more entry. [See them](docs/formats.md).
- **The page has its own light and dark**, apart from the app's, so a dark app can show a light page, like paper.

<table>
<tr>
<td width="50%" align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/readme/new-page-dark.png" />
<img alt="The New page dialog: three note formats and a small Blank page button" src="docs/images/readme/new-page.png" />
</picture>
<br /><sub><b>Start from a format, or a blank page</b></sub>
</td>
<td width="50%" align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/readme/export-note-dark.png" />
<img alt="The Export note dialog" src="docs/images/readme/export-note.png" />
</picture>
<br /><sub><b>Export a whole note, with its history</b></sub>
</td>
</tr>
<tr>
<td colspan="2" align="center">
<img alt="A light app with a dark page" src="docs/images/readme/page-look-dark-page.png" width="700" />
<br /><sub><b>The page has a look of its own: a dark page in a light app (and, at the top of this page, a light page in a dark app)</b></sub>
</td>
</tr>
</table>

## Built-in MCP server

Lucid has a built-in MCP server, so an AI agent such as Claude Code can read your pages and add to them. Everything an agent does
is one undo step and shows up in the history as the agent's. The tools and the set-up are in [docs/mcp.md](docs/mcp.md).

## Where it might go

Ideas, not promises: smaller, faster local models (5B to 8B, maybe Gemma) or cloud models through Ollama for a more live mode,
models fine-tuned for notes, and other subjects than system design. Voice stays in its own project, VoiceNative, and I may
integrate the two without merging them.

## Install

Expand Down
6 changes: 3 additions & 3 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,14 @@ A diagram of ten parts is hard to say in one breath, so there is a guided builde
cylinder, a queue as a pipe, a CDN as a cloud, a load balancer as a hexagon. Big diagrams stay readable in the page and in the
PDF. Tables have a builder in the same spirit.

![The diagram builder: numbered components and connections beside the drawing](images/diagram-builder.png)
![The diagram builder: numbered components and connections beside the drawing](images/readme/diagram-builder.png)

## Say which block you mean

Every block has a light number in the margin (B1, B2, …). Click one to see its parts, then say *"in block 3, add a row for
cost"* or *"cross out item 2 of block 6"*. The numbers can be hidden in Settings.

![Block numbers in the page margin; block 4, a diagram, lists its numbered parts and connections](images/block-numbers.png)
![Block numbers in the page margin; block 4, a diagram, lists its numbered parts and connections](images/readme/block-numbers.png)

## History you can trust

Expand All @@ -83,7 +83,7 @@ changes to other blocks alone.
- **Markdown** (diagrams as Mermaid blocks).
- **A `.lucid` file**, the whole note with its history, to back up, move to another computer or share.

![A page of the exported PDF: a numbered section, a diagram and a footer](images/pdf-page.png)
![A page of the exported PDF: a numbered section, a diagram and a footer](images/readme/pdf-page.png)

Light and dark themes, three typefaces for the page on screen, and a zoom for the page (⌘+, ⌘−, ⌘0) are in the app. Pages are
plain files in `~/Lucid` (one JSON file per page).
Expand Down
Binary file added docs/images/block-numbers-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/builder-connections-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/builder-connections.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/builder-edit-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/builder-edit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/builder-keywords-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/builder-keywords.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/export-note-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/export-note.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/page-look-dark-page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/page-look-light-page.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/block-numbers-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/block-numbers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/builder-connections-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/builder-connections.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/builder-edit-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/builder-edit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/builder-keywords-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/builder-keywords.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/diagram-builder-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/diagram-builder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/export-note-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/export-note.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/lucid-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/lucid-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/readme/new-page-dark.png
Binary file added docs/images/readme/new-page.png
Binary file added docs/images/readme/page-look-dark-page.png
Binary file added docs/images/readme/page-look-light-page.png
Binary file added docs/images/readme/pdf-page.png
59 changes: 59 additions & 0 deletions scripts/frame-images.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Makes the framed copies of the window pictures (docs/images/*.png -> docs/images/readme/*.png) that the README and the docs
// show: rounded corners and a thin edge, so a picture stands out on the page it sits on. A light picture on GitHub's white page
// gets a grey edge, and a dark one, which would melt into GitHub's dark page, gets a light edge (the edge is made to be seen on
// either page). The website frames its own pictures in CSS and uses the plain ones.
//
// node scripts/frame-images.mjs (needs Python with Pillow, as format-samples.mjs does)
import { execFileSync } from "node:child_process";
import { mkdirSync, readdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const from = join(root, "docs/images");
const to = join(from, "readme");
mkdirSync(to, { recursive: true });

// The social preview is a card on its own, not a window picture.
const skip = new Set(["social-preview.png", "poster.png"]);
const files = readdirSync(from).filter((f) => f.endsWith(".png") && !skip.has(f));
// The first picture of the README, a dark app with a light page, has a white edge: white, the dark app, the light page.
const white = ["page-look-light-page.png"];

execFileSync(
"python3",
[
"-c",
[
"import sys, os",
"from PIL import Image, ImageChops, ImageDraw, ImageStat",
"src, dst, white = sys.argv[1], sys.argv[2], sys.argv[3].split(',')",
"for name in sys.argv[4:]:",
" im = Image.open(os.path.join(src, name)).convert('RGB')",
" w, h = im.size",
" r = max(10, round(w / 150))",
" bw = max(2, round(w / 700))",
// Light or dark is read from the picture's own edge, not its name: a light app can hold a dark page.
" strips = [im.crop((0, 0, w, 6)), im.crop((0, h - 6, w, h)), im.crop((0, 0, 6, h)), im.crop((w - 6, 0, w, h))]",
" lum = sum(ImageStat.Stat(s.convert('L')).mean[0] for s in strips) / 4",
" edge = (214, 222, 232, 255) if lum < 110 else (170, 178, 189, 255)",
" if name in white: edge = (255, 255, 255, 255)",
" k = 4",
" def rounded(inset, radius):",
" m = Image.new('L', (w * k, h * k), 0)",
" ImageDraw.Draw(m).rounded_rectangle([inset * k, inset * k, (w - inset) * k - 1, (h - inset) * k - 1], radius=radius * k, fill=255)",
" return m.resize((w, h), Image.LANCZOS)",
" outer, inner = rounded(0, r), rounded(bw, max(r - bw, 1))",
" ring = ImageChops.subtract(outer, inner)",
" out = Image.composite(Image.new('RGBA', (w, h), edge), im.convert('RGBA'), ring)",
" out.putalpha(outer)",
" out.save(os.path.join(dst, name), optimize=True)",
" print(name, 'white' if name in white else 'dark' if lum < 110 else 'light')",
].join("\n"),
from,
to,
white.join(","),
...files,
],
{ stdio: "inherit" },
);
Loading
Loading