cassandrasestier commited on
Commit
5677abd
Β·
verified Β·
1 Parent(s): c110e66

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +167 -46
app.py CHANGED
@@ -1,6 +1,6 @@
1
  # ================================
2
  # πŸͺž MoodMirror+ β€” Text Emotion β€’ Advice-only + brief intros & reasons
3
- # - Tabs: Advice β€’ Emergency numbers β€’ Breathing β€’ Journal (export selected/delete/export all)
4
  # - Gradio + sklearn compatibility fixes
5
  # ================================
6
  import os
@@ -21,6 +21,15 @@ from sklearn.linear_model import LogisticRegression
21
  from sklearn.multiclass import OneVsRestClassifier
22
  from sklearn.pipeline import Pipeline
23
 
 
 
 
 
 
 
 
 
 
24
  # ---------------- Storage paths ----------------
25
  def _pick_data_dir():
26
  if os.path.isdir("/data") and os.access("/data", os.W_OK):
@@ -192,6 +201,19 @@ def journal_get(entry_id: int):
192
  ts, emo, title, content = row
193
  return {"ts": ts, "emotion": emo or "", "title": title or "", "content": content or ""}
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  def journal_delete(entry_id: int):
196
  conn = get_conn()
197
  cur = conn.execute("DELETE FROM journal WHERE id = ?", (int(entry_id),))
@@ -200,43 +222,111 @@ def journal_delete(entry_id: int):
200
  conn.close()
201
  return changes > 0
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  def journal_export_txt(entry_id: int):
204
  data = journal_get(entry_id)
205
  if not data:
206
  return None, "Entry not found."
207
  fname = f"journal_{entry_id}_{data['ts'].replace(':','-')}.txt"
208
  fpath = os.path.join(DATA_DIR, fname)
209
- lines = []
210
- title = data["title"] or "(Untitled)"
211
- lines.append(f"Title: {title}")
212
- lines.append(f"Emotion: {data['emotion'] or '-'}")
213
- lines.append(f"Saved (UTC): {data['ts']}")
214
- lines.append("-" * 40)
215
- lines.append(data["content"])
216
  with open(fpath, "w", encoding="utf-8") as f:
217
  f.write("\n".join(lines))
218
  return fpath, f"Ready: {fname}"
219
 
220
- def journal_export_all_zip():
 
 
 
 
 
 
 
 
 
 
 
 
221
  conn = get_conn()
222
- rows = list(conn.execute("SELECT id, ts, emotion, title, content FROM journal ORDER BY ts"))
223
  conn.close()
224
  if not rows:
225
  return None, "No entries to export."
 
 
226
  zip_name = os.path.join(DATA_DIR, "journal_all.zip")
227
  with zipfile.ZipFile(zip_name, "w", zipfile.ZIP_DEFLATED) as zf:
228
- for (id_, ts, emo, title, content) in rows:
229
- safe_title = re.sub(r"[^a-zA-Z0-9_\- ]", "_", title or "untitled")
230
- fname = f"{ts[:19].replace(':','-')} - {safe_title}.txt"
231
- text = (
232
- f"Title: {title or '(Untitled)'}\n"
233
- f"Emotion: {emo or '-'}\n"
234
- f"Saved (UTC): {ts}\n"
235
- f"{'-'*40}\n"
236
- f"{content or ''}"
237
- )
238
- zf.writestr(fname, text)
239
- return zip_name, f"Exported {len(rows)} entries."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
  # ---------------- Model ----------------
242
  def load_goemotions_dataset():
@@ -256,12 +346,7 @@ def train_or_load_model():
256
  ("tfidf", TfidfVectorizer(lowercase=True, ngram_range=(1,2), min_df=2, max_df=0.9, strip_accents="unicode")),
257
  ("ovr", OneVsRestClassifier(
258
  LogisticRegression(
259
- solver="saga",
260
- penalty="l2",
261
- C=0.5, # stronger regularization to help convergence
262
- tol=1e-3, # slightly looser tolerance
263
- max_iter=5000, # more iterations
264
- class_weight="balanced"
265
  ),
266
  n_jobs=-1
267
  ))
@@ -412,12 +497,13 @@ with gr.Blocks(title="πŸͺž MoodMirror+ β€” Text Emotion β€’ Advice-only") as dem
412
  yield "βœ… Done. Notice how your body feels."
413
  start_btn.click(run_breathing, inputs=[pattern, cycles], outputs=[breathe_out])
414
 
415
- # ---- Tab 4: Journal (export selected/delete/export all) ----
416
  with gr.Tab("Journal"):
417
- gr.Markdown("#### πŸ“ Personal journal\nWrite freely, then save. Entries are stored locally (SQLite).")
418
 
 
419
  with gr.Row():
420
- j_title = gr.Textbox(label="Title (optional)", placeholder="e.g., A tough day at work")
421
  j_emotion = gr.Dropdown(
422
  choices=["neutral","sadness","fear","anger","nervousness","boredom","grief","love","joy","curiosity","gratitude"],
423
  value="neutral", label="Emotion tag"
@@ -425,9 +511,13 @@ with gr.Blocks(title="πŸͺž MoodMirror+ β€” Text Emotion β€’ Advice-only") as dem
425
  j_text = gr.Textbox(lines=10, label="Your entry", placeholder="Write whatever you want to get off your chest...")
426
 
427
  with gr.Row():
428
- j_save = gr.Button("Save entry", variant="primary")
429
- j_status = gr.Markdown()
 
430
 
 
 
 
431
  gr.Markdown("##### Your entries")
432
  with gr.Row():
433
  j_search = gr.Textbox(label="Search (title, text or emotion)", placeholder="e.g., anxiety, work, joy")
@@ -436,13 +526,14 @@ with gr.Blocks(title="πŸͺž MoodMirror+ β€” Text Emotion β€’ Advice-only") as dem
436
  j_table = gr.Dataframe(headers=["UTC time","Emotion","Title","Preview"], value=[], interactive=False)
437
  j_view = gr.Markdown()
438
 
439
- # Export + Delete + Export all UI
440
  with gr.Row():
441
- j_export_btn = gr.Button("Export selected .txt")
442
- j_export_file = gr.File(label="Download exported entry", visible=True)
 
443
  j_delete_btn = gr.Button("Delete entry", variant="stop")
444
  with gr.Row():
445
- j_export_all_btn = gr.Button("⬇️ Export all entries (.zip)")
446
  j_export_all_file = gr.File(label="Download all entries", visible=True)
447
 
448
  # --------------- Backend ---------------
@@ -457,20 +548,38 @@ with gr.Blocks(title="πŸͺž MoodMirror+ β€” Text Emotion β€’ Advice-only") as dem
457
  clear_title = "" if ok else title
458
  return msg, drop, table, clear_text, clear_title
459
 
460
- def _load_entry(entry_id):
461
- if entry_id is None: return ""
 
 
 
 
 
 
 
462
  data = journal_get(entry_id)
463
- if not data: return "Entry not found."
 
464
  title_line = f"### {data['title']}" if data['title'] else "### (Untitled)"
465
  emo_line = f"**Emotion:** {data['emotion'] or 'β€”'} \n**Saved (UTC):** {data['ts']}"
466
- return f"{title_line}\n\n{emo_line}\n\n---\n\n{data['content']}"
 
467
 
468
- def _export_entry(entry_id):
 
 
 
469
  if entry_id is None:
470
- return None, "Select an entry to export."
471
  path, msg = journal_export_txt(int(entry_id))
472
  return path, msg
473
 
 
 
 
 
 
 
474
  def _delete_entry(entry_id, search):
475
  if entry_id is None:
476
  status = "Select an entry to delete."
@@ -482,19 +591,31 @@ with gr.Blocks(title="πŸͺž MoodMirror+ β€” Text Emotion β€’ Advice-only") as dem
482
  return status, drop, table, ""
483
 
484
  def _export_all():
485
- path, msg = journal_export_all_zip()
486
  return path, msg
487
 
488
  # Wire actions
489
  j_save.click(_save_entry, inputs=[j_title, j_text, j_emotion, j_search],
490
  outputs=[j_status, j_entries, j_table, j_text, j_title])
 
 
 
 
 
 
491
  j_refresh.click(_refresh_entries, inputs=[j_search], outputs=[j_entries, j_table])
492
  j_search.submit(_refresh_entries, inputs=[j_search], outputs=[j_entries, j_table])
493
- j_entries.change(_load_entry, inputs=[j_entries], outputs=[j_view])
494
 
495
- j_export_btn.click(_export_entry, inputs=[j_entries], outputs=[j_export_file, j_status])
 
 
 
 
 
 
496
  j_delete_btn.click(_delete_entry, inputs=[j_entries, j_search],
497
  outputs=[j_status, j_entries, j_table, j_view])
 
498
  j_export_all_btn.click(_export_all, outputs=[j_export_all_file, j_status])
499
 
500
  if __name__ == "__main__":
 
1
  # ================================
2
  # πŸͺž MoodMirror+ β€” Text Emotion β€’ Advice-only + brief intros & reasons
3
+ # - Tabs: Advice β€’ Emergency numbers β€’ Breathing β€’ Journal (edit + PDF export + export all)
4
  # - Gradio + sklearn compatibility fixes
5
  # ================================
6
  import os
 
21
  from sklearn.multiclass import OneVsRestClassifier
22
  from sklearn.pipeline import Pipeline
23
 
24
+ # --- Optional PDF deps ---
25
+ try:
26
+ from reportlab.lib.pagesizes import A4
27
+ from reportlab.pdfgen import canvas
28
+ from reportlab.lib.units import mm
29
+ REPORTLAB_OK = True
30
+ except Exception:
31
+ REPORTLAB_OK = False
32
+
33
  # ---------------- Storage paths ----------------
34
  def _pick_data_dir():
35
  if os.path.isdir("/data") and os.access("/data", os.W_OK):
 
201
  ts, emo, title, content = row
202
  return {"ts": ts, "emotion": emo or "", "title": title or "", "content": content or ""}
203
 
204
+ def journal_update(entry_id: int, title: str, content: str, emotion: str):
205
+ if entry_id is None: return False, "No entry selected."
206
+ title = (title or "").strip()
207
+ content = (content or "").strip()
208
+ if not content:
209
+ return False, "Entry content cannot be empty."
210
+ conn = get_conn()
211
+ cur = conn.execute("UPDATE journal SET title=?, content=?, emotion=? WHERE id=?", (title, content, emotion or "", int(entry_id)))
212
+ conn.commit()
213
+ ok = (cur.rowcount or 0) > 0
214
+ conn.close()
215
+ return ok, ("Updated βœ“" if ok else "Entry not found.")
216
+
217
  def journal_delete(entry_id: int):
218
  conn = get_conn()
219
  cur = conn.execute("DELETE FROM journal WHERE id = ?", (int(entry_id),))
 
222
  conn.close()
223
  return changes > 0
224
 
225
+ # --- PDF helpers ---
226
+ def _wrap_text(text, max_chars=90):
227
+ lines = []
228
+ for para in (text or "").split("\n"):
229
+ para = para.rstrip()
230
+ while len(para) > max_chars:
231
+ cut = para.rfind(" ", 0, max_chars)
232
+ if cut == -1: cut = max_chars
233
+ lines.append(para[:cut])
234
+ para = para[cut:].lstrip()
235
+ lines.append(para)
236
+ return lines
237
+
238
+ def _pdf_from_entry(path, data):
239
+ # Minimal PDF render with reportlab
240
+ c = canvas.Canvas(path, pagesize=A4)
241
+ width, height = A4
242
+ x_margin = 20*mm
243
+ y_margin = 20*mm
244
+ y = height - y_margin
245
+ def draw_line(s, size=11, bold=False, leading=14):
246
+ nonlocal y
247
+ if y < 30*mm:
248
+ c.showPage()
249
+ y = height - y_margin
250
+ c.setFont("Helvetica-Bold" if bold else "Helvetica", size)
251
+ c.drawString(x_margin, y, s)
252
+ y -= leading
253
+ # Header
254
+ title = data["title"] or "(Untitled)"
255
+ draw_line(f"Title: {title}", size=14, bold=True, leading=18)
256
+ draw_line(f"Emotion: {data['emotion'] or '-'}")
257
+ draw_line(f"Saved (UTC): {data['ts']}")
258
+ draw_line("-"*80)
259
+ # Body
260
+ for ln in _wrap_text(data["content"], 95):
261
+ draw_line(ln)
262
+ c.showPage()
263
+ c.save()
264
+
265
  def journal_export_txt(entry_id: int):
266
  data = journal_get(entry_id)
267
  if not data:
268
  return None, "Entry not found."
269
  fname = f"journal_{entry_id}_{data['ts'].replace(':','-')}.txt"
270
  fpath = os.path.join(DATA_DIR, fname)
271
+ lines = [
272
+ f"Title: {data['title'] or '(Untitled)'}",
273
+ f"Emotion: {data['emotion'] or '-'}",
274
+ f"Saved (UTC): {data['ts']}",
275
+ "-" * 40,
276
+ data["content"] or ""
277
+ ]
278
  with open(fpath, "w", encoding="utf-8") as f:
279
  f.write("\n".join(lines))
280
  return fpath, f"Ready: {fname}"
281
 
282
+ def journal_export_pdf(entry_id: int):
283
+ if not REPORTLAB_OK:
284
+ return None, "PDF export requires 'reportlab' (pip install reportlab)."
285
+ data = journal_get(entry_id)
286
+ if not data:
287
+ return None, "Entry not found."
288
+ safe_title = re.sub(r"[^a-zA-Z0-9_\- ]", "_", data["title"] or "untitled")
289
+ fname = f"journal_{entry_id}_{data['ts'][:19].replace(':','-')} - {safe_title}.pdf"
290
+ fpath = os.path.join(DATA_DIR, fname)
291
+ _pdf_from_entry(fpath, data)
292
+ return fpath, f"PDF ready: {fname}"
293
+
294
+ def journal_export_all_zip(pdf_first=True):
295
  conn = get_conn()
296
+ rows = list(conn.execute("SELECT id FROM journal ORDER BY ts"))
297
  conn.close()
298
  if not rows:
299
  return None, "No entries to export."
300
+ # Try PDFs if requested & available, else fall back to txt
301
+ use_pdf = pdf_first and REPORTLAB_OK
302
  zip_name = os.path.join(DATA_DIR, "journal_all.zip")
303
  with zipfile.ZipFile(zip_name, "w", zipfile.ZIP_DEFLATED) as zf:
304
+ for (entry_id,) in rows:
305
+ data = journal_get(entry_id)
306
+ if not data:
307
+ continue
308
+ safe_title = re.sub(r"[^a-zA-Z0-9_\- ]", "_", data["title"] or "untitled")
309
+ stamp = data["ts"][:19].replace(":", "-")
310
+ if use_pdf:
311
+ # write to a temp pdf then add
312
+ tmp_pdf = os.path.join(DATA_DIR, f"_tmp_{entry_id}.pdf")
313
+ _pdf_from_entry(tmp_pdf, data)
314
+ arcname = f"{stamp} - {safe_title}.pdf"
315
+ zf.write(tmp_pdf, arcname=arcname)
316
+ try: os.remove(tmp_pdf)
317
+ except: pass
318
+ else:
319
+ txt = (
320
+ f"Title: {data['title'] or '(Untitled)'}\n"
321
+ f"Emotion: {data['emotion'] or '-'}\n"
322
+ f"Saved (UTC): {data['ts']}\n"
323
+ f"{'-'*40}\n"
324
+ f"{data['content'] or ''}"
325
+ )
326
+ arcname = f"{stamp} - {safe_title}.txt"
327
+ zf.writestr(arcname, txt)
328
+ msg = "Exported all as PDF." if use_pdf else "Exported all as TXT (install 'reportlab' for PDF)."
329
+ return zip_name, msg
330
 
331
  # ---------------- Model ----------------
332
  def load_goemotions_dataset():
 
346
  ("tfidf", TfidfVectorizer(lowercase=True, ngram_range=(1,2), min_df=2, max_df=0.9, strip_accents="unicode")),
347
  ("ovr", OneVsRestClassifier(
348
  LogisticRegression(
349
+ solver="saga", penalty="l2", C=0.5, tol=1e-3, max_iter=5000, class_weight="balanced"
 
 
 
 
 
350
  ),
351
  n_jobs=-1
352
  ))
 
497
  yield "βœ… Done. Notice how your body feels."
498
  start_btn.click(run_breathing, inputs=[pattern, cycles], outputs=[breathe_out])
499
 
500
+ # ---- Tab 4: Journal (edit + PDF export + export all) ----
501
  with gr.Tab("Journal"):
502
+ gr.Markdown("#### πŸ“ Personal journal\nWrite freely, then save. Re-open to edit. Export as PDF or all in a ZIP.")
503
 
504
+ # Editor
505
  with gr.Row():
506
+ j_title = gr.Textbox(label="Title", placeholder="e.g., A tough day at work")
507
  j_emotion = gr.Dropdown(
508
  choices=["neutral","sadness","fear","anger","nervousness","boredom","grief","love","joy","curiosity","gratitude"],
509
  value="neutral", label="Emotion tag"
 
511
  j_text = gr.Textbox(lines=10, label="Your entry", placeholder="Write whatever you want to get off your chest...")
512
 
513
  with gr.Row():
514
+ j_save = gr.Button("Save as NEW", variant="primary")
515
+ j_update = gr.Button("Update selected", variant="secondary")
516
+ j_new = gr.Button("New (clear)")
517
 
518
+ j_status = gr.Markdown()
519
+
520
+ # Listing / search / preview
521
  gr.Markdown("##### Your entries")
522
  with gr.Row():
523
  j_search = gr.Textbox(label="Search (title, text or emotion)", placeholder="e.g., anxiety, work, joy")
 
526
  j_table = gr.Dataframe(headers=["UTC time","Emotion","Title","Preview"], value=[], interactive=False)
527
  j_view = gr.Markdown()
528
 
529
+ # Export / Delete
530
  with gr.Row():
531
+ j_export_txt_btn = gr.Button("Export selected .txt")
532
+ j_export_pdf_btn = gr.Button("Export selected PDF")
533
+ j_export_file = gr.File(label="Download exported file", visible=True)
534
  j_delete_btn = gr.Button("Delete entry", variant="stop")
535
  with gr.Row():
536
+ j_export_all_btn = gr.Button("⬇️ Export ALL entries (.zip, PDFs if possible)")
537
  j_export_all_file = gr.File(label="Download all entries", visible=True)
538
 
539
  # --------------- Backend ---------------
 
548
  clear_title = "" if ok else title
549
  return msg, drop, table, clear_text, clear_title
550
 
551
+ def _update_entry(entry_id, title, text, emotion, search):
552
+ ok, msg = journal_update(entry_id, title, text, emotion)
553
+ drop, table = _refresh_entries(search)
554
+ return msg, drop, table
555
+
556
+ def _load_entry_fill(entry_id):
557
+ """Load selected entry into editor + preview."""
558
+ if entry_id is None:
559
+ return "", "neutral", "", ""
560
  data = journal_get(entry_id)
561
+ if not data:
562
+ return "", "neutral", "", "Entry not found."
563
  title_line = f"### {data['title']}" if data['title'] else "### (Untitled)"
564
  emo_line = f"**Emotion:** {data['emotion'] or 'β€”'} \n**Saved (UTC):** {data['ts']}"
565
+ preview = f"{title_line}\n\n{emo_line}\n\n---\n\n{data['content']}"
566
+ return data["title"], data["emotion"] or "neutral", data["content"], preview
567
 
568
+ def _new_entry():
569
+ return "", "neutral", "", "Cleared. You can write a new entry."
570
+
571
+ def _export_txt(entry_id):
572
  if entry_id is None:
573
+ return None, "Select an entry to export (.txt)."
574
  path, msg = journal_export_txt(int(entry_id))
575
  return path, msg
576
 
577
+ def _export_pdf(entry_id):
578
+ if entry_id is None:
579
+ return None, "Select an entry to export (PDF)."
580
+ path, msg = journal_export_pdf(int(entry_id))
581
+ return path, msg
582
+
583
  def _delete_entry(entry_id, search):
584
  if entry_id is None:
585
  status = "Select an entry to delete."
 
591
  return status, drop, table, ""
592
 
593
  def _export_all():
594
+ path, msg = journal_export_all_zip(pdf_first=True)
595
  return path, msg
596
 
597
  # Wire actions
598
  j_save.click(_save_entry, inputs=[j_title, j_text, j_emotion, j_search],
599
  outputs=[j_status, j_entries, j_table, j_text, j_title])
600
+
601
+ j_update.click(_update_entry, inputs=[j_entries, j_title, j_text, j_emotion, j_search],
602
+ outputs=[j_status, j_entries, j_table])
603
+
604
+ j_new.click(_new_entry, outputs=[j_title, j_emotion, j_text, j_view])
605
+
606
  j_refresh.click(_refresh_entries, inputs=[j_search], outputs=[j_entries, j_table])
607
  j_search.submit(_refresh_entries, inputs=[j_search], outputs=[j_entries, j_table])
 
608
 
609
+ # Selecting an entry both fills the editor and shows preview
610
+ j_entries.change(_load_entry_fill, inputs=[j_entries],
611
+ outputs=[j_title, j_emotion, j_text, j_view])
612
+
613
+ j_export_txt_btn.click(_export_txt, inputs=[j_entries], outputs=[j_export_file, j_status])
614
+ j_export_pdf_btn.click(_export_pdf, inputs=[j_entries], outputs=[j_export_file, j_status])
615
+
616
  j_delete_btn.click(_delete_entry, inputs=[j_entries, j_search],
617
  outputs=[j_status, j_entries, j_table, j_view])
618
+
619
  j_export_all_btn.click(_export_all, outputs=[j_export_all_file, j_status])
620
 
621
  if __name__ == "__main__":