Integrating AI into LifeLab
In this article I will chronicle various ways I tried to integrate AI (==LLM) into LifeLab, a personal note taking app, and show ways it failed.
*Reasons against AI integration
I was initially strongly against integrating it, because:
1. It is easy and unrewarding from a hobbyist-coder standpoint. Integrating LLM is 1 npm install away and I didn't imagine many interesting vibe-decisions to be had. I did not think I can make a unique take on it so it would just become another tackled on feature. It reminded me of people who install openclaw just so they can claim they work with openclaw, without doing anything interesting with it.
2. It goes against the spirit of taking notes as a thinking tool. Just making LLM do the thinking and planning for you defeats the purpose of writing stuff down in the first place. I feel a lot of "chat with your notes" style integrations just seem like pasting your diary into chatGPT interface and don't have actionable outcomes.
*Reasons for integration
I changed my mind for two and a half reasons.
1. If it were a truly malleable app, it should be able to be edited using Claude from within itself without using claude code externally. I really wanted to prove this approach would work and demonstrate a automagical experience where I ask AI to add a visualization from data and it would just do it, no need to copy paste from another chat window.
2. If everything is a block in a database, can we dynamically compose prompts from blocks? Can this system evolve? I thought I could make a interesting spin on LLM from this.
3. Hubris. Maybe I can make something more interesting than the Notion Assistant or CraftDocs assistant chat windows? Maybe my block api design is powerful enough to create something cool? Both companies likely have experienced UX designers, user surveys and analytics data to figure out optimal user flows and needs, but I got taste and enthusiasm. This goes against the first point I had but I am fickle.
AI as notebook API
The first thing I did was storing the api key serverside, and exposing an API via my notebook object. Then I can call the configured AI from code blocks. Code blocks can already query and edit other blocks, and be installed as hooks to trigger on system and user events. I could make dynamic experiences like guesstimating number of calories from photos by tagging a photo with ":macros:" tag. After adding a tag, a registered codeblock automatically takes the content of the block, an image, and feeds it to an LLM with a hardcoded prompt. Afterwards it updates the block title with a description, and the properties as calories. That worked fairly elegantly. I think a self contained workflow that can be triggered at will is a great use case.
I thought if we polish this a bit more, nb.ai() can be a great first class utility, even though it defeats the purpose of coding a bit. It is so much more convenient than installing huggingface and loading a model locally. Under this block is a photo I had claude automatically describe and try to estimate calories.
Example of AI code blocks modifying image blocks after tagging:extracted_text:
Image
:EXTRACTED_TEXT:(4 lines)
4 lines·173 charactersCopy
Large breaded fried schnitzel with roasted potato slices and arugula salad
:journal/2?26-03-05: food:macros:
PROPERTIES: CALORIES: 875  CARBS: 68  FAT: 48
PROTETN: 42 : END:
:END:
Customer service chat
Then, I thought I can make an interesting way to chat with your notes. I added a chat button with a dynamic prompt composed of all blocks on the current page. LifeLab has an api surface to also search and edit blocks and I exposed all of those calls as tools to the model, which would be cooler than just "Chat with your notes". The theme was "AI and user use the same api surface to work on notes", but it wasn't so useful because the AI would just query all notes and run out of context. There was a lot of prompting needed to turn this into a good experience.
I also did not like the transient nature of the chats. You can save a chat as a block by tapping a button but by default it disappears on refresh. I felt I am burning tokens and not storing them but that would later turn out to be a hoarding problem.
What truly killed this feature was when my wife told me it looks like a customer support chat. I deleted it immediately afterwards.
Customer service chat screenshot:extracted_text:
Image
:EXTRACTED_TEXT:(43 lines)
43 lines·1,795 charactersCopy
blog /lifelab
bottom bar
 Add taa schema
All Feb 261 Feb 2514
: iournal/2026-02-25:
learned 0.0001% of UX and now reality is poison
in this article  go through many mobile bottom navigation bars for lifelab_I started W
elements that are in a side bar under a hamburger menu.  got very simple feedback fri Al claude-sonnet-4-5-20250929 bloa/lifelab/bottom bar Save
eft me franticallv obssess about round corners and concentric alignment and ruined
Rate mv block acrticle
1 Peak dork but usable
l'll read through your article about the bottom ba
UX journey and give you some feedback!
This is a really engaging and honest article!
Here's my rating and breakdown:
## Overall: 8/10
### What Works Reallv Well
**Authentic Voice** Your honest, slightlv self-
deprecating tone ("Peak dork but usable"
earhed 0.0001% of UX") is refreshing and
relatable. It makes the article fun to read
**Great Visual Documentation** You included
screenshots/recordings at each stage, which
makes it easv to follow your progression and see
exactly what vou're talking about.
**Specific Learnings**The concrete feedback
you received (concer Scroll to bottom al VS
diagonal animation) aives readers actua
Ask anything...
In the first version Claude decided to make the buttons pop out of the bottom bar a tin
usable.liked not wasting space with floating bar and wanted to push If to the bottom
maximize screen estate. The real Issue was mv block styling, but Ilearned that later
V7
:iournal/2026-02-25:
V2 Semi translucent round floaty bar
V2
E :journa1 /2026-02-25:
V3 More feedback
My friend gave me following feedback:
1. Decouple search button from the navigation bar
2. The shrinking animation should track vertically (how do you even realize this stuff?
3. Use icons instead of emojis to work with the colors m
Send
:END:
AI as first class block
Following the previous fiasco, I tried to implement ai as a first class block. LifeLab currently has note blocks for text content, code blocks for.. code, mermaid diagram blocks, html blocks, image blocks, each has a different way to visualize and modify its content. Why not AI block? It would persist the entire conversation, allowing me to continue chats if needed. It would be collapsible and have a sense of belonging by being a explicit block on a page.
It made absolutely sense on a conceptual level, but the experience was horrific, because it would have to fit (the overly verbose) AI output in a fixed height block, so it was a lot of scrolling. There was also no iteration. The AI block can update and create blocks but could not edit them because the prompt was only created and populated at the beginning of the chat, there was no easy way to refresh. Additionally, it kept hallucinating APIs and code that wouldn't work, making it a painful experience.
AI block example:extracted_text:
Image
:EXTRACTED_TEXT:(17 lines)
17 lines·474 charactersCopy
Classic Cacio e Pepe Recipe
#tbegin al
System Prompt
help me create a pasta recipe on this page
can help you add a pasta recipe to this page
Goal: Add a structured recipe block for pasta
Proposed approach:
Create a new note block with a clear recipe format
Include ingredients list. instructions, and serving info
Position It naturally on the page
Recipe structure:
Title and brief description
Ingredients (with auantities
Cton hu atoninstruations
Ask lab mate
#tend a1
Send
:END:
AI as inline slashcommand
When I removed the chat field, I did not realize some tasks are transient and I ended up with this workflow where I create AI blocks, prompt them it to create a block with code, and then delete the original AI block, which is even worse than before.
As a minor compromise to avoid the humiliation of customer support chat, I added a inline slash command that updates the existing block only. Since most of the changes I actually want is programming, I added the command to the code block. By typing /ai it shows a small prompt menu to describe my change and then the AI gives me a diff which I can see and confirm.
This was incredibly practical. Usually, each block is a self contained script with no dependencies, so it's all you need to feed to the model. The diff view is useful at a glance, the reject/apply pattern is familiar... it's familiar to claude code. Even in a self-hosted, personal note taking and scripting app, I want claude code. Claude code is the new default state of coding. I implemented a language server and autocomplete to show function calls, but what I really wanted the whole time is to prompt the notebook API spec into claude and have it generate code for me, making all my previous work somewhat pointless.
It's hard to pretend AI-assisted coding is not a thing and stubbornly make a tool that supports old fashioned coding, when it's everywhere. So I leaned into it. We now have ghetto claude code in the code blocks which are no longer used for coding, just for prompting claude to write code for us.
AI assisted coding with diff view:extracted_text:
Image
:EXTRACTED_TEXT:(27 lines)
27 lines·931 charactersCopy
FOR -- NOTT ON JOURNAL PAGE, QUERY FEBRUARY 2026, THE FLOW DOES NOT WORK BECAUSE WE LATER CHECK TF +18 
CURRENT PAGE AND CURRENT PAGE:MATCH("JOURNAL/") THEN; WHICH FAILS
Reject Apply
Query all blocks that have sentiment metadata and aggregate by date
Local current page nbcurrent page
local blocks
local month label "Monthly View"
local days 1n month = 31
local year, month
Raw
-22
Check if we re on a Journa page and extract date range
if current page and current page:match("journal/") then
local date str = current page:match ("/+") - extract "2025-10-31"
J111001107 00
10er nh uy uaee .   D
year, month= date SLI:match("(dododdd)- (odod) "
if vear and month then
year, month = tonumber (year) tonumber (month)
local first day = string. format ("?04d-?02d-01", year, month)
8 unchanged lines
F)
end
#+beg1n_srC Lua
1 - Query all blocks that have sentiment metadata and aggregate by date
2 local current page nb. current page
Run
:END:
Prompt and docs
Finally, I have had massive problems maintaining the API documentation. Since it's vibe generated and vibe verified, it often ended up incorrect and drifting apart. Claude would imagine and describe functionality that was not based on the real API behavior. Every time I found a fix, I would need to rebuild the rust code, redeploy docker and wait for my app to relaunch.
One major help was to turn the hardcoded documentation into text blocks, and aggregate them using tags. These are now dynamic and I can change the documentation from within the app without having to relaunch.
Transcription blocks
Finally, one useful AI-adjacent feature is a transcription block by using soniox speech API. I never heard of them before and initially wanted to use eleven labs before, because some of my coworkers went there, but ElevenLabs had notably worse API and did not do translation. Soniox is working really well! I use transcription blocks to transcribe AND translate chinese text for learning purposes. The resulting transcriptions can also be mined using code blocks to make flashcards, and I can implement a spaced repetition system within LifeLab to memorize them.
Example transcription from mandarin
transcript zh, en → en
我要吃冰淇淋。
I want to eat ice cream.
我没有钱。
I don't have money.
Conclusion (human written)
In conclusion, I learned a bit about AI use different patterns and by avoiding the customer service chat slop, I stepped into a worse puddle of compressing the entire thing into a tiny block and ended up realizing the chat pattern has value and use for times you need to quickly edit things.
I learned not all conversations require saving and that I should not hoard information for no reason. After this reflection, I ended up with a new pattern -> Embed AI inline exactly where it is needed the most. No need for a separate block or chat window. This scopes the capabilities and expectations better.
I also learned that claude-code-like coding is the absolute future and no one cares about my stupid autocomplete suggestions with code snippets, when they can just prompt entire codebases into existence.
I still wonder if there is value in a archivist / historian style agent that maintains your notes, but I have not yet found a compelling use cases worth obsessing over.
Self-Aware Post About AI Block Workflow Missteps
--- retired AI conversation ---
You:
do you see any typos or logical fallacies in this blog post?
Assistant:
I'll review the blog post content for typos and logical issues.
**Typos/grammar:**
1. "It is easy and unrewarding from hobbyist-coder standpoint" → should be "**a** hobbyist-coder standpoint"
2. "each has a different way to visualize and modify **it's** content" → should be "**its** content" (possessive, no apostrophe)
3. "I stepped into a worse puddle of compressing the entire thing into a tiny block which is even worse" — "worse" is used twice redundantly in the same sentence. Consider: "I stepped into a puddle of compressing the entire thing into a tiny block, which turned out even worse."
**Logical issues:**
1. The conclusion says you avoided customer service chat and ended up with something worse (the AI block workflow of create→prompt→delete). But the narrative actually shows you iterated through multiple approaches, each solving different problems. Framing it as purely "stepped into a worse puddle" undersells that the slash command and inline AI block ended up being genuine improvements. The self-deprecation is charming but slightly contradicts the actual progress described.
2. There's a schnitzel image in the middle of the post with no connection to the surrounding content — intentional? 🍗
No major logical fallacies — the post is mostly a narrative retrospective rather than an argument, so there isn't much to be fallacious about. The reasoning is sound: you identified real UX problems with each approach and iterated.
You:
why you gotta call me out like this
Assistant:
Hey, you asked me to look! 😄 But honestly the post is self-aware about the missteps — that's what makes it a good read. The "I avoided customer service chat slop and ended up with something worse" arc is genuinely funny and relatable.
P.S. api reference example
I have also added 1 example of the system prompt that is collected and composed into the system and can be modified during runtime. it describes calling AI from code blocks.
AI:api-doc::prompt/builder::prompt/inline:
***nb.ai
Signature: nb.ai(prompt, system, model, max_tokens) → str
Call an LLM and return response text. Uses AI provider/model/key from Settings. (Python only)
Parameters:
- prompt (str): Prompt text to send to the LLM.
- system (str (optional)): System prompt for the LLM.
- model (str (optional)): Model override (uses Settings default).
- max_tokens (int (optional)): Max response tokens. Default: 4096.
Example:
```python
# Simple prompt
summary = nb.ai("Summarize: " + text)
# With system prompt
analysis = nb.ai(
data,
system="You are a data analyst."
)
```
Output:
```
"The text discusses three main themes..."
```