Add diet estimates command

This commit is contained in:
Ben Stuart 2025-02-08 23:22:16 -06:00
parent e8a8602c60
commit 8b0dfcf154
11 changed files with 212 additions and 28 deletions

View file

@ -4,16 +4,19 @@ This plugin for Obsidian adds AI commands for daily journals.
![](https://github.com/benstuart0/on-this-day-i-obsidian/blob/e9f8ad5cb0165089f8bcb8717d4a0a7d033192b7/on-this-day-i-demo.gif)
## Features
This plugin comes with two commands.
This plugin comes with two commands:
### Add Placeholder at Cursor
Adds a custom tag at your cursor location which the generation command looks for and replaces if it is available. The tag can be changed in settings.
Adds a custom tag at your cursor location which the generation command looks for and replaces if it is available. The tag can be changed in settings. A tag command and setting should be available for each generation command.
### Generate Through The Years
Uses AI to find Daily Notes from the same date as your current note in years past. Generates a brief summary of this date for each instance of it through the years and places it in your note.
### Generate Diet Estimates
Parse your daily journal for food-related lines, and create a `dietEstimates` code-block that estimates your daily calories and macros.
## How to Use
- Enable the plugin in the settings menu and update the plugin settings accordingly
- Enable the plugin in the settings menu and update the plugin settings accordingly.
- Open the command palette and search for On This Day I to see the commands.
- If you want the output to be placed at a certain location in your note, add a placeholder (default is <!OTDI>, can be changed in settings). Run the `On This Day I: Add Placeholder at Cursor` command if you'd like to automatically add a placeholder to your note at cursor location.
- Run the command `On This Day I: Generate Through The Years` to generate your yearly insights.
- Run the command `On This Day I: Generate Diet Estimates` to generate a calorie and macro estimates code-block.
### OpenAI
- **This plugin makes a network call to the OpenAI API to generate the journal summaries.**
- Must have OpenAI API key. See [platform.openai.com](https://platform.openai.com/). Add this key to plugin settings.
@ -22,6 +25,8 @@ Uses AI to find Daily Notes from the same date as your current note in years pas
- Must be run from a note with a valid Date format. Update the date format in settings to match your daily note format.
- Daily notes must all follow the same date format (e.g, MMMM D, YYYY would be something like February 6, 2025)
- There must be at least one other daily note from this day in a past year.
### Diet Estimates Requirements
- Meant to be run on a daily journal that includes information documenting what you have eaten in a day.
## How to Install
### From within Obsidian
You can activate this plugin within Obsidian by doing the following:

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View file

Before

Width:  |  Height:  |  Size: 7.9 MiB

After

Width:  |  Height:  |  Size: 7.9 MiB

139
main.ts
View file

@ -2,9 +2,13 @@ import { MarkdownView, Notice, Plugin, TFolder } from "obsidian";
import OpenAI from "openai";
import { OnThisDayPluginSettings, DEFAULT_SETTINGS } from "src/settings";
import OnThisDaySettingTab from "src/OnThisDaySettingTab";
import { sysPrompt, constructPrompt } from "src/prompt";
import { sysPrompt, constructPrompt, constructDietPrompt } from "src/prompt";
import { isValidDateString, parseDateFromString } from "src/dateUtils";
import { getMarkdownFilesInFolder, buildOutputBlock } from "src/markdownUtils";
import {
getMarkdownFilesInFolder,
buildOutputBlock,
buildHealthOutputBlock,
} from "src/markdownUtils";
export default class OnThisDayPlugin extends Plugin {
settings: OnThisDayPluginSettings;
@ -14,10 +18,18 @@ export default class OnThisDayPlugin extends Plugin {
this.addSettingTab(new OnThisDaySettingTab(this.app, this));
this.addCommand({
id: "on-this-day-placeholder",
name: "Add Placeholder at Cursor",
id: "through-the-years-placeholder",
name: "Add Through The Years Placeholder at Cursor",
callback: () => {
this.addPlaceholder();
this.addThroughTheYearsPlaceholder();
},
});
this.addCommand({
id: "diet-estimates-placeholder",
name: "Add Diet Estimates Placeholder at Cursor",
callback: () => {
this.addDietEstimatesPlaceholder();
},
});
@ -28,6 +40,14 @@ export default class OnThisDayPlugin extends Plugin {
await this.generateDateSummaries();
},
});
this.addCommand({
id: "diet-estimates",
name: "Generate Diet Estimates",
callback: async () => {
await this.generateDietEstimates();
},
});
}
async loadSettings() {
@ -42,9 +62,6 @@ export default class OnThisDayPlugin extends Plugin {
await this.saveData(this.settings);
}
//
// Main Function: Generate Date Summaries
//
async generateDateSummaries() {
// Ensure the command is run from a file with a valid date as its title.
const activeFile = this.app.workspace.getActiveFile();
@ -168,9 +185,97 @@ export default class OnThisDayPlugin extends Plugin {
new Notice("On This Day summaries inserted.");
}
//
// Call the OpenAI API
//
async generateDietEstimates() {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) {
new Notice("No active file found.");
return;
}
// Read the file content.
const content = await this.app.vault.read(activeFile);
// Define keywords that typically indicate food or meal entries.
const foodKeywords = [
"breakfast",
"lunch",
"dinner",
"snack",
"mid-day",
"food",
"ate",
"eating",
"protein",
];
// Split the note into lines and filter lines that include any of these keywords.
const foodLines = content
.split("\n")
.filter((line) =>
foodKeywords.some((keyword) =>
line.toLowerCase().includes(keyword)
)
)
.join("\n");
// (Optionally, if no lines are found, notify the user.)
if (!foodLines) {
new Notice("No food-related details found in the note.");
return;
}
// Build the prompt for the OpenAI API.
const prompt = constructDietPrompt(foodLines);
// Show loading indicator
const loadingNotice = new Notice(
"Calculating diet estimates... Please wait.",
0
);
// Call the OpenAI API
let responseJSON: Record<string, any>;
try {
responseJSON = await this.callOpenAI(prompt);
} catch (error: any) {
loadingNotice.hide();
console.error("Diet estimates calculation failed:", error);
new Notice("Diet estimates calculation failed: " + error.message);
return;
}
loadingNotice.hide();
new Notice("Diet estimates calculated!");
console.log("RESPONSE:\n", responseJSON);
// Build the output markdown block.
const outputBlock = buildHealthOutputBlock(
responseJSON.calories,
responseJSON.protein,
responseJSON.carbs,
responseJSON.fats,
responseJSON.est_deficit
);
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeView) {
loadingNotice.hide();
new Notice("No active editor found.");
return;
}
const editor = (activeView as MarkdownView).editor;
// Insert the output block at a placeholder if available, else at cursor location
const placeholder = this.settings.dietEstimatePlaceholder;
if (content.includes(placeholder)) {
const newContent = content.replace(placeholder, outputBlock);
editor.setValue(newContent);
} else {
editor.replaceSelection(outputBlock);
}
new Notice("Diet estimates inserted into the note.");
}
async callOpenAI(prompt: string): Promise<Record<string, string>> {
const client = new OpenAI({
apiKey: this.settings.openaiApiKey,
@ -218,7 +323,7 @@ export default class OnThisDayPlugin extends Plugin {
}
}
addPlaceholder() {
addThroughTheYearsPlaceholder() {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeView) {
new Notice("No active editor found.");
@ -227,4 +332,14 @@ export default class OnThisDayPlugin extends Plugin {
const editor = (activeView as MarkdownView).editor;
editor.replaceSelection(this.settings.placeholder);
}
addDietEstimatesPlaceholder() {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeView) {
new Notice("No active editor found.");
return;
}
const editor = (activeView as MarkdownView).editor;
editor.replaceSelection(this.settings.dietEstimatePlaceholder);
}
}

View file

@ -1,9 +1,9 @@
{
"id": "on-this-day-i",
"name": "On This Day I",
"version": "1.0.1",
"version": "1.1.0",
"minAppVersion": "1.8.4",
"description": "Summarize a calendar date in years past with Daily Journals",
"description": "AI tools for Daily Journals",
"author": "Ben Stuart",
"authorUrl": "https://github.com/benstuart0",
"fundingUrl": "",

View file

@ -1,7 +1,7 @@
{
"name": "on-this-day-i",
"version": "1.0.1",
"description": "This is the first version of the 'On This Day I' Obsidian plugin",
"version": "1.1.0",
"description": "AI tools for Daily Journals",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",

View file

@ -45,7 +45,7 @@ export default class OnThisDaySettingTab extends PluginSettingTab {
// Get all files from the vault and filter for folders
const allFolders = this.app.vault
.getAllLoadedFiles()
.filter((f) => f instanceof TFolder) as TFolder[];
.filter((f): f is TFolder => f instanceof TFolder);
// Populate the folderOptions with folder paths
allFolders.forEach((folder) => {
folderOptions[folder.path] = folder.path;
@ -122,10 +122,12 @@ export default class OnThisDaySettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Link to yearly notes in output")
.setDesc("If enabled, includes [[]] style links to previous years' notes in output block")
.setDesc(
"If enabled, includes [[]] style links to previous years' notes in output block"
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.shouldOutputLinkToNotes)
@ -161,5 +163,20 @@ export default class OnThisDaySettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
})
);
containerEl.createEl("h2", { text: "Health Estimate Settings" });
new Setting(containerEl)
.setName("Health Estimates Placeholder Tag")
.setDesc(
"The plugin will search for this text in your active file to replace with its return block. Otherwise outputs at cursor. Default <!OTDI health>"
)
.addText((text) =>
text
.setValue(this.plugin.settings.dietEstimatePlaceholder)
.onChange(async (value) => {
this.plugin.settings.dietEstimatePlaceholder = value;
await this.plugin.saveSettings();
})
);
}
}

View file

@ -56,3 +56,20 @@ export function buildOutputBlock(
return output;
}
export function buildHealthOutputBlock(
calories: string,
protein: string,
carbs: string,
fats: string,
deficit: string
): string {
const outputBlock =
`\`\`\`dietEstimates
calories: ${calories}
protein: ${protein}
carbs: ${carbs}
fats: ${fats}
\`\`\``;
return outputBlock;
}

View file

@ -1,8 +1,15 @@
const userPrompt =
"Below is a JSON object where each key is a year and each value is the full content of my daily journal for this day on that given year. For each year, please provide an interesting summary in 2-3 sentences that highlights the events of that day. Try to include who I spent time with (including their names), what I did, and how I felt. Use second-person language (you), as you are referring to me - the writer of the journals. Do not include the date in the summary. Return only a JSON object mapping each year to its summary with no extra text, headers, or footers. This is the most important rule. The output must follow this JSON format. Further personalization details: ";
const userPrompt = `Below is a JSON object where each key is a year and each value is the full
content of my daily journal for this day on that given year. For each year,
please provide an interesting summary in 2-3 sentences that highlights the events
of that day. Try to include who I spent time with (including their names), what I did,
and how I felt. Use second-person language (you), as you are referring to me -
the writer of the journals. Do not include the date in the summary.
Return only a JSON object mapping each year to its summary with no extra text,
headers, or footers. This is the most important rule. The output must follow this JSON format.
Further personalization details: `;
export const sysPrompt =
"You are a helpful assistant that summarizes text, based on a user's specifications, always in a strict JSON format.";
export const sysPrompt = `You are a helpful assistant that summarizes text,
based on a user's specifications, always in a strict JSON format.`;
//
// Construct the API Prompt, add user setting if needed
@ -15,3 +22,20 @@ export function constructPrompt(
const dataString = JSON.stringify(yearToContent);
return `${promptInstructions}\n\nData:\n${dataString}`;
}
export function constructDietPrompt(foodInfo: string): string {
const prompt = `You are a health assistant. Based on the following food details extracted from a daily journal,
provide an estimated nutritional analysis.
Food Information:
${foodInfo}
Provide the output in JSON format with the following keys:
- calories: estimated total calories consumed,
- protein: estimated grams of protein,
- carbs: estimated grams of carbohydrates,
- fats: estimated grams of fats,
Return only a JSON object with these keys, with no additional text. Include units in the JSON values.`;
return prompt;
}

View file

@ -2,8 +2,10 @@
// Settings Interfaces & Defaults
//
export interface OnThisDayPluginSettings {
// OpenAI settings
openaiApiKey: string;
model: string;
// General settings
dailyNotesFolder: string;
dateFormat: string;
horizontalRules: string;
@ -11,6 +13,8 @@ export interface OnThisDayPluginSettings {
placeholder: string;
throughTheYearsHeader: string;
shouldOutputLinkToNotes: boolean;
// Health estimate settings
dietEstimatePlaceholder: string;
}
export const DEFAULT_SETTINGS: OnThisDayPluginSettings = {
@ -22,5 +26,6 @@ export const DEFAULT_SETTINGS: OnThisDayPluginSettings = {
customPrompt: "",
placeholder: "<!OTDI>",
throughTheYearsHeader: "On This Day",
shouldOutputLinkToNotes: true
shouldOutputLinkToNotes: true,
dietEstimatePlaceholder: "<!OTDI diet>"
};

View file

@ -1,4 +1,5 @@
{
"1.0.0": "1.8.4",
"1.0.1": "1.8.4"
"1.0.1": "1.8.4",
"1.1.0": "1.8.4"
}