eloigil6 commited on
Commit
f6bca03
·
1 Parent(s): 0ec83ba

Refactor notes functionality for shared family access

Browse files

- Updated the notes API to remove member-specific access, allowing for a single shared family notebook.
- Modified frontend components to reflect the shared nature of notes, enhancing the user experience.
- Adjusted serialization and storage methods to support the new shared notes structure.
- Incremented asset versioning in index.html to ensure proper caching.

This commit improves the notes feature by centralizing access for all family members, fostering collaboration and ease of use.

app.py CHANGED
@@ -126,9 +126,9 @@ def recipes() -> list:
126
 
127
 
128
  @app.api(name="notes")
129
- def notes(member: str = "elena") -> list:
130
- """A member's notes, projected as note cards."""
131
- return serialize.notes(member)
132
 
133
 
134
  @app.api(name="meal_plan")
 
126
 
127
 
128
  @app.api(name="notes")
129
+ def notes() -> list:
130
+ """The shared family notes, projected as note cards."""
131
+ return serialize.notes()
132
 
133
 
134
  @app.api(name="meal_plan")
frontend/app/app.jsx CHANGED
@@ -18,7 +18,7 @@ const TITLES = {
18
  calendar: ["Calendar", "The shared family week"],
19
  recipes: ["Recipes", "Your recipe box, stored as plain Markdown"],
20
  meals: ["Meal plan", "A week of lunches and dinners, planned and shopped for"],
21
- notes: ["Notes", "A private space for each person"],
22
  settings: ["Settings", "Your family, stored locally in data/members.json"],
23
  };
24
 
@@ -218,9 +218,7 @@ function App() {
218
  {route === "meals" && (
219
  <Meals plan={plan} onOpenRecipe={(r) => setRecipeOpen(r)} />
220
  )}
221
- {route === "notes" && (
222
- <Notes activeMember={active} members={members} />
223
- )}
224
  {route === "settings" && (
225
  <Settings
226
  members={members}
 
18
  calendar: ["Calendar", "The shared family week"],
19
  recipes: ["Recipes", "Your recipe box, stored as plain Markdown"],
20
  meals: ["Meal plan", "A week of lunches and dinners, planned and shopped for"],
21
+ notes: ["Notes", "One shared notebook for the whole family"],
22
  settings: ["Settings", "Your family, stored locally in data/members.json"],
23
  };
24
 
 
218
  {route === "meals" && (
219
  <Meals plan={plan} onOpenRecipe={(r) => setRecipeOpen(r)} />
220
  )}
221
+ {route === "notes" && <Notes />}
 
 
222
  {route === "settings" && (
223
  <Settings
224
  members={members}
frontend/app/live.jsx CHANGED
@@ -20,7 +20,7 @@ const MFLive = {
20
  return res.data[0];
21
  },
22
  recipes() { return this._call("/recipes", {}); },
23
- notes(member) { return this._call("/notes", { member }); },
24
  mealPlan() { return this._call("/meal_plan", {}); },
25
  members() { return this._call("/members", {}); },
26
  addMember(name, foodPreferences) {
 
20
  return res.data[0];
21
  },
22
  recipes() { return this._call("/recipes", {}); },
23
+ notes() { return this._call("/notes", {}); },
24
  mealPlan() { return this._call("/meal_plan", {}); },
25
  members() { return this._call("/members", {}); },
26
  addMember(name, foodPreferences) {
frontend/app/screens3.jsx CHANGED
@@ -122,71 +122,47 @@ function NoteCard({ note }) {
122
  );
123
  }
124
 
125
- function Notes({ activeMember, members }) {
126
- const roster = members || [];
127
- const [who, setWho] = useState(activeMember);
128
- useEffect(() => { if (activeMember) setWho(activeMember); }, [activeMember]);
129
-
130
- // Pull every member's notes live. Re-runs when the roster changes so a newly
131
- // added member's (empty) notes show up. 'shared' has no backend file yet.
132
- const [liveMap, setLiveMap] = useState(null);
133
- const rosterKey = roster.map(m => m.id).join(",");
134
  useEffect(() => {
135
  let alive = true;
136
- let busy = false;
137
- const run = () => {
138
- if (busy) return;
139
- busy = true;
140
- Promise.all(roster.map(m =>
141
- MFLive.notes(m.id).then(n => [m.id, n]).catch(() => [m.id, null])
142
- )).then(pairs => {
143
- if (!alive) return;
144
- const map = {};
145
- pairs.forEach(([k, v]) => { if (v != null) map[k] = v; });
146
- setLiveMap(map);
147
- }).finally(() => { busy = false; });
148
- };
149
  run();
150
- const off = MFRefresh.subscribe(run); // pick up hand-edited notes files
151
  return () => { alive = false; off(); };
152
- }, [rosterKey]);
153
- const notesFor = (id) => (liveMap && liveMap[id]) || [];
154
-
155
- const tabs = [...roster, { id: "shared", name: "Shared", color: "var(--ash)", init: "•", full: "Whole family" }];
156
- const notes = notesFor(who);
157
 
158
  return (
159
  <div className="view-wide fade-in">
160
  <div className="notes-layout">
161
  <div>
162
- <div className="note-members">
163
- {tabs.map(t => (
164
- <button key={t.id} className={`note-mtab ${who === t.id ? "on" : ""}`} onClick={() => setWho(t.id)}>
165
- {t.id === "shared" ? <Icon name="home" size={17} style={{ color: "var(--ash)" }} /> : <Avatar member={t} size="xs" />}
166
- {t.id === "shared" ? "Shared" : t.name}
167
- <span className="ct">{notesFor(t.id).length}</span>
168
- </button>
169
- ))}
170
- </div>
171
- <button className="btn btn-ink btn-sm" style={{ width: "100%", marginTop: 14 }}><Icon name="plus" size={15} /> New note</button>
172
  <div className="muted" style={{ fontSize: 11, marginTop: 14, lineHeight: 1.6, padding: "0 4px" }}>
173
- Each person's notes live in their own Markdown file. Ask Mini Fam to read or add to any of them.
 
174
  </div>
175
  </div>
176
 
177
  <div>
178
  <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16 }}>
179
- {who !== "shared" ? <Avatar member={who} size="md" /> : <div className="av av-md" style={{ "--c": "var(--ash)" }}>•</div>}
 
 
180
  <div>
181
- <div style={{ fontSize: 18, fontWeight: 600 }}>{who === "shared" ? "Shared notes" : memberById(who).name + "'s notes"}</div>
182
- <div className="muted mono" style={{ fontSize: 11.5, marginTop: 2 }}>{who === "shared" ? "shared/notes.md" : `members/${who}/notes.md`}</div>
183
  </div>
184
  </div>
185
- {notes.length ? (
186
  <div className="note-cols">
187
- {notes.map((n, i) => <NoteCard note={n} key={who + i} />)}
188
  </div>
189
- ) : <div className="empty">No notes yet.</div>}
190
  </div>
191
  </div>
192
  </div>
 
122
  );
123
  }
124
 
125
+ function Notes() {
126
+ // One shared notebook for the whole family (data/shared/notes.md).
127
+ const [notes, setNotes] = useState(null);
 
 
 
 
 
 
128
  useEffect(() => {
129
  let alive = true;
130
+ const run = () => MFLive.notes().then((n) => { if (alive) setNotes(n); }).catch(() => {});
 
 
 
 
 
 
 
 
 
 
 
 
131
  run();
132
+ const off = MFRefresh.subscribe(run); // pick up agent / hand edits
133
  return () => { alive = false; off(); };
134
+ }, []);
135
+ const list = notes || [];
 
 
 
136
 
137
  return (
138
  <div className="view-wide fade-in">
139
  <div className="notes-layout">
140
  <div>
141
+ <SectionH icon="notes" title="Family notes" />
142
+ <button className="btn btn-ink btn-sm" style={{ width: "100%", marginTop: 14 }}>
143
+ <Icon name="plus" size={15} /> New note
144
+ </button>
 
 
 
 
 
 
145
  <div className="muted" style={{ fontSize: 11, marginTop: 14, lineHeight: 1.6, padding: "0 4px" }}>
146
+ One shared notebook for the whole family, stored as Markdown at
147
+ <span className="mono"> shared/notes.md</span>. Ask Mini Fam to add to it.
148
  </div>
149
  </div>
150
 
151
  <div>
152
  <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16 }}>
153
+ <div className="av av-md" style={{ "--c": "var(--ash)" }}>
154
+ <Icon name="home" size={18} style={{ color: "#fff" }} />
155
+ </div>
156
  <div>
157
+ <div style={{ fontSize: 18, fontWeight: 600 }}>Shared notes</div>
158
+ <div className="muted mono" style={{ fontSize: 11.5, marginTop: 2 }}>shared/notes.md</div>
159
  </div>
160
  </div>
161
+ {list.length ? (
162
  <div className="note-cols">
163
+ {list.map((n, i) => <NoteCard note={n} key={i} />)}
164
  </div>
165
+ ) : <div className="empty">No notes yet — ask Mini Fam to jot one down.</div>}
166
  </div>
167
  </div>
168
  </div>
frontend/index.html CHANGED
@@ -4,11 +4,11 @@
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>Mini Fam — the family assistant that stays home</title>
7
- <link rel="icon" type="image/png" href="app/logo.png?v=17" />
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
  <link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
11
- <link rel="stylesheet" href="app/minifam.css?v=17" />
12
  </head>
13
  <body>
14
  <div id="root"></div>
@@ -26,12 +26,12 @@
26
  <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
27
  <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
28
 
29
- <script type="text/babel" src="app/seed.jsx?v=17"></script>
30
- <script type="text/babel" src="app/ui.jsx?v=17"></script>
31
- <script type="text/babel" src="app/live.jsx?v=17"></script>
32
- <script type="text/babel" src="app/screens1.jsx?v=17"></script>
33
- <script type="text/babel" src="app/screens2.jsx?v=17"></script>
34
- <script type="text/babel" src="app/screens3.jsx?v=17"></script>
35
- <script type="text/babel" src="app/app.jsx?v=17"></script>
36
  </body>
37
  </html>
 
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>Mini Fam — the family assistant that stays home</title>
7
+ <link rel="icon" type="image/png" href="app/logo.png?v=19" />
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
  <link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
11
+ <link rel="stylesheet" href="app/minifam.css?v=19" />
12
  </head>
13
  <body>
14
  <div id="root"></div>
 
26
  <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
27
  <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
28
 
29
+ <script type="text/babel" src="app/seed.jsx?v=19"></script>
30
+ <script type="text/babel" src="app/ui.jsx?v=19"></script>
31
+ <script type="text/babel" src="app/live.jsx?v=19"></script>
32
+ <script type="text/babel" src="app/screens1.jsx?v=19"></script>
33
+ <script type="text/babel" src="app/screens2.jsx?v=19"></script>
34
+ <script type="text/babel" src="app/screens3.jsx?v=19"></script>
35
+ <script type="text/babel" src="app/app.jsx?v=19"></script>
36
  </body>
37
  </html>
minifam/agent.py CHANGED
@@ -20,7 +20,7 @@ from . import config, tools
20
  client = OpenAI(base_url=config.LLM_BASE_URL, api_key=config.LLM_API_KEY)
21
 
22
  SYSTEM_PROMPT = """You are MiniFam, a warm and concise assistant that runs \
23
- entirely on a family's own computer. You help family members keep personal \
24
  notes, manage a shared recipe cookbook, plan weekly meals — lunch and dinner \
25
  for each day, always respecting each person's dietary preferences — build \
26
  shopping lists, and keep a shared family calendar of events and tasks.
 
20
  client = OpenAI(base_url=config.LLM_BASE_URL, api_key=config.LLM_API_KEY)
21
 
22
  SYSTEM_PROMPT = """You are MiniFam, a warm and concise assistant that runs \
23
+ entirely on a family's own computer. You help the family keep shared \
24
  notes, manage a shared recipe cookbook, plan weekly meals — lunch and dinner \
25
  for each day, always respecting each person's dietary preferences — build \
26
  shopping lists, and keep a shared family calendar of events and tasks.
minifam/serialize.py CHANGED
@@ -145,16 +145,13 @@ def _recipe_by_title(query: str) -> dict | None:
145
  return _recipe_from_post(f.stem, frontmatter.load(str(f)))
146
 
147
 
148
- def notes(member: str) -> list[dict]:
149
- """A member's notes file projected as a single checklist card.
150
 
151
- Real notes are an append-only log of "- [stamp] text" lines; we surface them
152
- as one tickable card so the screen reads well and stays honest to the file.
153
  """
154
- m = config.member(member)
155
- if not m:
156
- return []
157
- raw = storage.list_notes(m["id"])
158
  items, last = [], ""
159
  for line in raw.splitlines():
160
  match = re.match(r"-\s*\[([^\]]*)\]\s*(.*)", line.strip())
@@ -167,7 +164,7 @@ def notes(member: str) -> list[dict]:
167
  {
168
  "type": "check",
169
  "title": "Reminders",
170
- "color": m["color"],
171
  "updated": f"Updated {last}" if last else "",
172
  "items": items,
173
  }
 
145
  return _recipe_from_post(f.stem, frontmatter.load(str(f)))
146
 
147
 
148
+ def notes() -> list[dict]:
149
+ """The shared family notebook projected as a single checklist card.
150
 
151
+ Notes are an append-only log of "- [stamp] text" lines; we surface them as
152
+ one tickable card so the screen reads well and stays honest to the file.
153
  """
154
+ raw = storage.list_notes()
 
 
 
155
  items, last = [], ""
156
  for line in raw.splitlines():
157
  match = re.match(r"-\s*\[([^\]]*)\]\s*(.*)", line.strip())
 
164
  {
165
  "type": "check",
166
  "title": "Reminders",
167
+ "color": "#7a9c6e",
168
  "updated": f"Updated {last}" if last else "",
169
  "items": items,
170
  }
minifam/storage.py CHANGED
@@ -26,26 +26,27 @@ def _write_frontmatter(path: Path, metadata: dict, body: str) -> None:
26
  path.write_text(f"---\n{fm}\n---\n\n{body}\n", encoding="utf-8")
27
 
28
 
29
- def _member_dir(member: str) -> Path:
30
- d = config.DATA_DIR / "members" / member.lower()
 
31
  d.mkdir(parents=True, exist_ok=True)
32
- return d
33
 
34
 
35
- def add_note(member: str, text: str) -> str:
36
- path = _member_dir(member) / "notes.md"
37
  stamp = datetime.now().strftime("%Y-%m-%d %H:%M")
38
  with path.open("a", encoding="utf-8") as f:
39
  f.write(f"- [{stamp}] {text}\n")
40
- return f"Saved a note for {member}."
41
 
42
 
43
- def list_notes(member: str) -> str:
44
- path = _member_dir(member) / "notes.md"
45
  if not path.exists():
46
- return f"{member} has no notes yet."
47
  content = path.read_text(encoding="utf-8").strip()
48
- return content or f"{member} has no notes yet."
49
 
50
 
51
  # --- Recipes -----------------------------------------------------------------
 
26
  path.write_text(f"---\n{fm}\n---\n\n{body}\n", encoding="utf-8")
27
 
28
 
29
+ def _notes_file() -> Path:
30
+ """One shared notebook for the whole family: data/shared/notes.md."""
31
+ d = config.DATA_DIR / "shared"
32
  d.mkdir(parents=True, exist_ok=True)
33
+ return d / "notes.md"
34
 
35
 
36
+ def add_note(text: str) -> str:
37
+ path = _notes_file()
38
  stamp = datetime.now().strftime("%Y-%m-%d %H:%M")
39
  with path.open("a", encoding="utf-8") as f:
40
  f.write(f"- [{stamp}] {text}\n")
41
+ return "Saved a note."
42
 
43
 
44
+ def list_notes() -> str:
45
+ path = _notes_file()
46
  if not path.exists():
47
+ return "No notes yet."
48
  content = path.read_text(encoding="utf-8").strip()
49
+ return content or "No notes yet."
50
 
51
 
52
  # --- Recipes -----------------------------------------------------------------
minifam/tools/notes.py CHANGED
@@ -1,4 +1,7 @@
1
- """Per-member note-taking tools."""
 
 
 
2
 
3
  from . import tool
4
  from .. import storage
@@ -10,8 +13,8 @@ from .. import storage
10
  "function": {
11
  "name": "add_note",
12
  "description": (
13
- "Save a personal note or reminder for a family member. "
14
- "Use whenever someone wants to remember or jot down something."
15
  ),
16
  "parameters": {
17
  "type": "object",
@@ -20,10 +23,6 @@ from .. import storage
20
  "type": "string",
21
  "description": "The note content to save.",
22
  },
23
- "member": {
24
- "type": "string",
25
- "description": "Who the note is for. Omit to use the current user.",
26
- },
27
  },
28
  "required": ["text"],
29
  },
@@ -31,8 +30,7 @@ from .. import storage
31
  }
32
  )
33
  def add_note(args: dict, context: dict) -> str:
34
- member = args.get("member") or context["member"]
35
- return storage.add_note(member, args["text"])
36
 
37
 
38
  @tool(
@@ -40,19 +38,10 @@ def add_note(args: dict, context: dict) -> str:
40
  "type": "function",
41
  "function": {
42
  "name": "list_notes",
43
- "description": "List the saved notes for a family member.",
44
- "parameters": {
45
- "type": "object",
46
- "properties": {
47
- "member": {
48
- "type": "string",
49
- "description": "Whose notes to list. Omit to use the current user.",
50
- },
51
- },
52
- },
53
  },
54
  }
55
  )
56
  def list_notes(args: dict, context: dict) -> str:
57
- member = args.get("member") or context["member"]
58
- return storage.list_notes(member)
 
1
+ """Shared family note-taking tools.
2
+
3
+ Notes are a single shared notebook for the whole household (data/shared/
4
+ notes.md) — not per person."""
5
 
6
  from . import tool
7
  from .. import storage
 
13
  "function": {
14
  "name": "add_note",
15
  "description": (
16
+ "Save a note or reminder to the family's shared notebook. Use "
17
+ "whenever someone wants to jot down or remember something."
18
  ),
19
  "parameters": {
20
  "type": "object",
 
23
  "type": "string",
24
  "description": "The note content to save.",
25
  },
 
 
 
 
26
  },
27
  "required": ["text"],
28
  },
 
30
  }
31
  )
32
  def add_note(args: dict, context: dict) -> str:
33
+ return storage.add_note(args["text"])
 
34
 
35
 
36
  @tool(
 
38
  "type": "function",
39
  "function": {
40
  "name": "list_notes",
41
+ "description": "List the family's shared notes.",
42
+ "parameters": {"type": "object", "properties": {}},
 
 
 
 
 
 
 
 
43
  },
44
  }
45
  )
46
  def list_notes(args: dict, context: dict) -> str:
47
+ return storage.list_notes()