Spaces:
Sleeping
Sleeping
| # ================================ | |
| # 🪞 MoodMirror+ — Emotion-aware advice | |
| # Tabs: Advice • Emergency numbers • Breathing • Journal | |
| # ================================ | |
| import os | |
| import re | |
| import random | |
| import sqlite3 | |
| import joblib | |
| import numpy as np | |
| import time | |
| import zipfile | |
| from datetime import datetime | |
| import gradio as gr | |
| from datasets import load_dataset | |
| from sklearn.feature_extraction.text import TfidfVectorizer | |
| from sklearn.preprocessing import MultiLabelBinarizer | |
| from sklearn.linear_model import LogisticRegression | |
| from sklearn.multiclass import OneVsRestClassifier | |
| from sklearn.pipeline import Pipeline | |
| # ---------------- Storage paths ---------------- | |
| def _pick_data_dir(): | |
| if os.path.isdir("/data") and os.access("/data", os.W_OK): | |
| return "/data" | |
| return os.getcwd() | |
| DATA_DIR = _pick_data_dir() | |
| os.makedirs(DATA_DIR, exist_ok=True) | |
| DB_PATH = os.path.join(DATA_DIR, "moodmirror.db") | |
| MODEL_PATH = os.path.join(DATA_DIR, "goemo_sklearn.joblib") | |
| MODEL_VERSION = "v13-all-maps-hints" | |
| # ---------------- Crisis & closing ---------------- | |
| CRISIS_RE = re.compile( | |
| r"\b(self[- ]?harm|suicid|kill myself|end my life|overdose|cutting|i don.?t want to live|can.?t go on)\b", | |
| re.I, | |
| ) | |
| CLOSING_RE = re.compile( | |
| r"\b(thanks?|thank you|bye|goodbye|see you|take care|ok bye|no thanks?)\b", | |
| re.I, | |
| ) | |
| CRISIS_NUMBERS_EN = { | |
| "United States": "📞 **988** (Suicide & Crisis Lifeline, 24/7)", | |
| "Canada": "📞 **988** (Suicide Crisis Helpline, 24/7)", | |
| "United Kingdom": "📞 **116 123** (Samaritans, 24/7)", | |
| "Ireland": "📞 **116 123** (Samaritans Ireland, 24/7)", | |
| "France": "📞 **3114** (National Suicide Prevention number, 24/7)", | |
| "Belgium": "📞 **1813** (Zelfmoordlijn, 24/7)", | |
| "Switzerland": "📞 **143** (La Main Tendue / Heart2Heart)", | |
| "Spain": "📞 **024** (Línea 024 — Atención a la conducta suicida, 24/7)", | |
| "Germany": "📞 **0800 111 0 111** / **0800 111 0 222** / **116 123** (TelefonSeelsorge, 24/7)", | |
| "Netherlands": "📞 **0800-0113** (free) or **113** (standard rate) — 113 Suicide Prevention", | |
| "Portugal": "📞 **213 544 545**, **912 802 669**, **963 524 660** (SOS Voz Amiga)", | |
| "Australia": "📞 **13 11 14** (Lifeline, 24/7)", | |
| "New Zealand": "📞 **0508 828 865** (Suicide Crisis Helpline — TAUTOKO)", | |
| "India": "📞 **14416** (Tele MANAS, 24/7) or **1800-599-0019** (KIRAN)", | |
| "South Africa": "📞 **0800 567 567** (SADAG Suicide Crisis Helpline, 24/7)", | |
| "Other / Not listed": "Call local emergency (**112/911**) or search “suicide hotline” + your country.", | |
| } | |
| # ---------------- Advice library (5 tips each) ---------------- | |
| SUGGESTIONS = { | |
| "sadness": [ | |
| "Go for a 5-minute outside walk and name three colors you see.", | |
| "Write what hurts, then add one thing you still care about.", | |
| "Take a warm shower and focus on your shoulders relaxing.", | |
| "Text a safe person: “Can I vent for 2 minutes?”", | |
| "Wrap in a blanket and slow your exhale for 60 seconds.", | |
| ], | |
| "fear": [ | |
| "Do 5-4-3-2-1 grounding: 5 see, 4 feel, 3 hear, 2 smell, 1 taste.", | |
| "Make your exhale longer than your inhale for eight breaths.", | |
| "Hold something cool (spoon/ice) for 30 seconds and notice the sensation.", | |
| "Name the fear in one clear sentence out loud.", | |
| "Write the worst case, then the most likely case beside it.", | |
| ], | |
| "anger": [ | |
| "Take space before replying; set a 10-minute timer.", | |
| "Do ten slow exhales through pursed lips.", | |
| "Squeeze then release your fists ten times.", | |
| "Walk fast for five minutes or do one stair flight.", | |
| "Write the crossed boundary; draft one calm sentence.", | |
| ], | |
| "nervousness": [ | |
| "4-7-8 breathing: in 4s, hold 7s, out 8s (four rounds).", | |
| "Relax your jaw and lower your shoulders.", | |
| "Write worries down; underline what you can control.", | |
| "Pick one tiny task you can finish in five minutes.", | |
| "Hold a warm mug and notice the heat and weight.", | |
| ], | |
| "boredom": [ | |
| "Set a 2-minute timer and start anything small.", | |
| "Change your soundtrack—put on one new song.", | |
| "Do 15 jumping jacks or a quick stretch.", | |
| "Clean your phone screen or keyboard.", | |
| "Write five quick ideas without editing.", | |
| ], | |
| "grief": [ | |
| "Hold a photo or object and say their name softly.", | |
| "Drink water and eat something—your body grieves too.", | |
| "Write a short letter to them about today.", | |
| "Create a tiny ritual (song, candle, place).", | |
| "Plan one kind thing for yourself this week.", | |
| ], | |
| "love": [ | |
| "Send a kind message without expecting a reply.", | |
| "Note three things you appreciate about someone close.", | |
| "Offer yourself one gentle act you needed today.", | |
| "Give a sincere compliment to a stranger.", | |
| "Plan a tiny gesture for tomorrow.", | |
| ], | |
| "joy": [ | |
| "Pause and take three slow breaths to savor this.", | |
| "Capture it—photo, note, or voice memo.", | |
| "Tell someone why you feel good right now.", | |
| "Move to music for one song.", | |
| "Plan a tiny celebration later today.", | |
| ], | |
| "curiosity": [ | |
| "Search one concept and read just the first paragraph.", | |
| "Write three quick “what if…?” ideas.", | |
| "Watch a “how does X work?” video for 3 minutes.", | |
| "Learn one new word and use it once.", | |
| "Sketch a simple diagram of an idea.", | |
| ], | |
| "gratitude": [ | |
| "List three tiny things that made today easier.", | |
| "Thank someone by name for something specific.", | |
| "Notice an everyday object and appreciate its help.", | |
| "Write “I’m lucky that…” and complete it once.", | |
| "Savor your next sip or bite with attention.", | |
| ], | |
| "neutral": [ | |
| "Take one slow breath and relax your hands.", | |
| "Stand, stretch, and roll your shoulders.", | |
| "Drink a glass of water mindfully.", | |
| "Organize three items in your space.", | |
| "Set a 10-minute timer to focus on one thing.", | |
| ], | |
| } | |
| # full GoEmotions → bucket | |
| GOEMO_TO_APP = { | |
| "admiration": "gratitude", | |
| "amusement": "joy", | |
| "anger": "anger", | |
| "annoyance": "anger", | |
| "approval": "gratitude", | |
| "caring": "love", | |
| "confusion": "nervousness", | |
| "curiosity": "curiosity", | |
| "desire": "joy", | |
| "disappointment": "sadness", | |
| "disapproval": "anger", | |
| "disgust": "anger", | |
| "embarrassment": "nervousness", | |
| "excitement": "joy", | |
| "fear": "fear", | |
| "gratitude": "gratitude", | |
| "grief": "grief", | |
| "joy": "joy", | |
| "love": "love", | |
| "nervousness": "nervousness", | |
| "optimism": "joy", | |
| "pride": "joy", | |
| "realization": "neutral", | |
| "relief": "gratitude", | |
| "remorse": "grief", | |
| "sadness": "sadness", | |
| "surprise": "neutral", | |
| "neutral": "neutral", | |
| } | |
| # ---------------- Preprocessing & Hints ---------------- | |
| CLEAN_RE = re.compile(r"(https?://\S+)|(@\w+)|(#\w+)|[^a-zA-Z0-9\s']") | |
| EMOJI_HINTS = {"😭": "sadness", "😡": "anger", "🥰": "love", "😨": "fear", "😴": "boredom"} | |
| HINTS_EN = { | |
| "i'm nervous": "nervousness", "im nervous": "nervousness", "nervous": "nervousness", | |
| "anxious": "nervousness", "anxiety": "nervousness", "panic": "nervousness", | |
| "i'm grieving": "grief", "im grieving": "grief", "grieving": "grief", "grief": "grief", | |
| "sad": "sadness", "depressed": "sadness", | |
| "angry": "anger", "furious": "anger", | |
| "afraid": "fear", "scared": "fear", | |
| } | |
| def clean_text(s: str) -> str: | |
| s = s.lower() | |
| s = CLEAN_RE.sub(" ", s) | |
| s = re.sub(r"\s+", " ", s).strip() | |
| return s | |
| def augment_text(text: str, history=None) -> str: | |
| t = clean_text(text or "") | |
| lt = (text or "").lower() | |
| tags = [] | |
| for k, v in EMOJI_HINTS.items(): | |
| if k in lt: tags.append(v) | |
| for k, v in HINTS_EN.items(): | |
| if k in lt: tags.append(v) | |
| if history and len(t.split()) < 8: | |
| prev_user = history[-1][0] if history and history[-1] else "" | |
| if isinstance(prev_user, str) and prev_user: | |
| t += " " + clean_text(prev_user) | |
| if tags: | |
| t += " " + " ".join(f"emo_{x}" for x in tags) | |
| return t | |
| # ---------------- SQLite ---------------- | |
| def get_conn(): | |
| return sqlite3.connect(DB_PATH, check_same_thread=False, timeout=10) | |
| def init_db(): | |
| conn = get_conn() | |
| conn.execute("""CREATE TABLE IF NOT EXISTS sessions( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| ts TEXT, country TEXT, user_text TEXT, main_emotion TEXT | |
| )""") | |
| conn.execute("""CREATE TABLE IF NOT EXISTS journal( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| ts TEXT NOT NULL, | |
| emotion TEXT, | |
| title TEXT, | |
| content TEXT | |
| )""") | |
| conn.commit() | |
| conn.close() | |
| def log_session(country, msg, emotion): | |
| conn = get_conn() | |
| conn.execute("INSERT INTO sessions(ts,country,user_text,main_emotion)VALUES(?,?,?,?)", | |
| (datetime.utcnow().isoformat(timespec='seconds'), country, (msg or "")[:500], emotion)) | |
| conn.commit() | |
| conn.close() | |
| # ---- Journal helpers ---- | |
| def journal_save(title: str, content: str, emotion: str): | |
| title = (title or "").strip() | |
| content = (content or "").strip() | |
| if not content: | |
| return False, "Please write something before saving." | |
| ts = datetime.utcnow().isoformat(timespec='seconds') | |
| conn = get_conn() | |
| conn.execute("INSERT INTO journal(ts, emotion, title, content) VALUES (?,?,?,?)", | |
| (ts, emotion or "", title, content)) | |
| conn.commit() | |
| conn.close() | |
| return True, f"Saved ✓ ({ts} UTC)." | |
| def journal_list(search: str = "", limit: int = 50): | |
| q = "SELECT id, ts, emotion, title, content FROM journal" | |
| params = [] | |
| if search: | |
| q += " WHERE (LOWER(title) LIKE ? OR LOWER(content) LIKE ? OR LOWER(emotion) LIKE ?)" | |
| s = f"%{search.lower()}%" | |
| params = [s, s, s] | |
| q += " ORDER BY ts DESC LIMIT ?" | |
| params.append(int(limit)) | |
| conn = get_conn() | |
| rows = list(conn.execute(q, params)) | |
| conn.close() | |
| options, table = [], [] | |
| for (id_, ts, emo, title, content) in rows: | |
| label = f"{ts} — [{(emo or 'neutral')}] {title or (content[:30] + ('…' if len(content) > 30 else ''))}" | |
| options.append((label, id_)) | |
| preview = (content or "").replace("\n", " ") | |
| if len(preview) > 120: preview = preview[:120] + "…" | |
| table.append([ts, emo or "—", title or "—", preview]) | |
| return options, table | |
| def journal_get(entry_id: int): | |
| conn = get_conn() | |
| cur = conn.execute("SELECT ts, emotion, title, content FROM journal WHERE id = ?", (int(entry_id),)) | |
| row = cur.fetchone() | |
| conn.close() | |
| if not row: return None | |
| ts, emo, title, content = row | |
| return {"ts": ts, "emotion": emo or "", "title": title or "", "content": content or ""} | |
| def journal_export_all_zip(): | |
| conn = get_conn() | |
| rows = list(conn.execute("SELECT id, ts, emotion, title, content FROM journal ORDER BY ts")) | |
| conn.close() | |
| if not rows: | |
| return None, "No entries to export." | |
| zip_name = os.path.join(DATA_DIR, "journal_all.zip") | |
| with zipfile.ZipFile(zip_name, "w", zipfile.ZIP_DEFLATED) as zf: | |
| for (id_, ts, emo, title, content) in rows: | |
| safe_title = re.sub(r"[^a-zA-Z0-9_\- ]", "_", title or "untitled") | |
| fname = f"{ts[:19].replace(':','-')} - {safe_title}.txt" | |
| text = ( | |
| f"Title: {title or '(Untitled)'}\n" | |
| f"Emotion: {emo or '-'}\n" | |
| f"Saved (UTC): {ts}\n" | |
| f"{'-'*40}\n" | |
| f"{content or ''}" | |
| ) | |
| zf.writestr(fname, text) | |
| return zip_name, f"Exported {len(rows)} entries." | |
| # ---------------- Model ---------------- | |
| def load_goemotions_dataset(): | |
| ds = load_dataset("google-research-datasets/go_emotions", "simplified") | |
| return ds, ds["train"].features["labels"].feature.names | |
| def train_or_load_model(): | |
| if os.path.exists(MODEL_PATH): | |
| bundle = joblib.load(MODEL_PATH) | |
| if bundle.get("version") == MODEL_VERSION: | |
| return bundle["pipeline"], bundle["mlb"], bundle["label_names"] | |
| ds, names = load_goemotions_dataset() | |
| X_train, y_train = ds["train"]["text"], ds["train"]["labels"] | |
| mlb = MultiLabelBinarizer(classes=list(range(len(names)))) | |
| Y_train = mlb.fit_transform(y_train) | |
| clf = Pipeline([ | |
| ("tfidf", TfidfVectorizer(lowercase=True, ngram_range=(1,2), min_df=2, max_df=0.9, strip_accents="unicode")), | |
| ("ovr", OneVsRestClassifier( | |
| LogisticRegression( | |
| solver="saga", | |
| penalty="l2", | |
| C=0.5, | |
| tol=1e-3, | |
| max_iter=5000, | |
| class_weight="balanced" | |
| ), | |
| n_jobs=-1 | |
| )) | |
| ]) | |
| clf.fit(X_train, Y_train) | |
| joblib.dump({"version": MODEL_VERSION, "pipeline": clf, "mlb": mlb, "label_names": names}, MODEL_PATH) | |
| return clf, mlb, names | |
| try: | |
| CLASSIFIER, MLB, LABEL_NAMES = train_or_load_model() | |
| except Exception as e: | |
| print("[ERROR] Model load/train:", e) | |
| CLASSIFIER, MLB, LABEL_NAMES = None, None, None | |
| def classify_text(text_augmented: str): | |
| if not CLASSIFIER: return [] | |
| proba = CLASSIFIER.predict_proba([text_augmented])[0] | |
| max_p = float(np.max(proba)) if len(proba) else 0.0 | |
| thr = max(0.10, 0.30 * max_p + 0.15) | |
| idxs = [i for i, p in enumerate(proba) if p >= thr] or [int(np.argmax(proba))] | |
| idxs.sort(key=lambda i: proba[i], reverse=True) | |
| return [(LABEL_NAMES[i], float(proba[i])) for i in idxs] | |
| def detect_emotion_text(message: str, history): | |
| labels = classify_text(augment_text(message, history)) | |
| if not labels: | |
| return "neutral" | |
| bucket = {} | |
| for lbl, p in labels: | |
| app = GOEMO_TO_APP.get(lbl.lower(), "neutral") | |
| bucket[app] = max(bucket.get(app, 0.0), p) | |
| return max(bucket, key=bucket.get) if bucket else "neutral" | |
| # ---------------- Advice engine ---------------- | |
| def pick_advice_from_pool(emotion: str, pool: dict, last_tip: str = ""): | |
| tips_all = SUGGESTIONS.get(emotion, SUGGESTIONS["neutral"]) | |
| entry = pool.get(emotion, {"unused": [], "last": ""}) | |
| if not entry["unused"]: | |
| refill = [t for t in tips_all if t != entry.get("last","")] or tips_all[:] | |
| random.shuffle(refill) | |
| entry["unused"] = refill | |
| tip = entry["unused"].pop(0) | |
| entry["last"] = tip | |
| pool[emotion] = entry | |
| return tip, pool | |
| def format_reply(emotion: str, tip: str) -> str: | |
| # removed "why it helps" | |
| return f"Try this now:\n• {tip}" | |
| def crisis_block_en(country): | |
| msg = CRISIS_NUMBERS_EN.get(country, CRISIS_NUMBERS_EN["Other / Not listed"]) | |
| return "💛 You matter. If you're in danger or thinking of harming yourself, please reach out now.\n\n" + msg | |
| def chat_step(user_text, history, country, save_session, advice_pool): | |
| if user_text and CRISIS_RE.search(user_text): | |
| return crisis_block_en(country), "neutral", "neutral", "", advice_pool | |
| if user_text and CLOSING_RE.search(user_text): | |
| emotion = "neutral" | |
| tip, advice_pool = pick_advice_from_pool(emotion, advice_pool) | |
| reply = format_reply(emotion, tip) | |
| return reply, "neutral", emotion, tip, advice_pool | |
| emotion = detect_emotion_text(user_text or "", history) | |
| if save_session: | |
| log_session(country, user_text or "", emotion) | |
| tip, advice_pool = pick_advice_from_pool(emotion, advice_pool) | |
| reply = format_reply(emotion, tip) | |
| return reply, emotion, emotion, tip, advice_pool | |
| # ---------------- UI ---------------- | |
| init_db() | |
| with gr.Blocks(title="🪞 MoodMirror+") as demo: | |
| gr.Markdown("### 🪞 MoodMirror+ — Emotion-aware advice\n_Not medical advice._") | |
| with gr.Tabs(): | |
| # ---- Advice ---- | |
| with gr.Tab("Advice"): | |
| with gr.Row(): | |
| country = gr.Dropdown(list(CRISIS_NUMBERS_EN.keys()), value="United States", label="Country") | |
| save_ok = gr.Checkbox(False, label="Save anonymized session") | |
| chat = gr.Chatbot(type="tuples", height=380) | |
| msg = gr.Textbox(label="Your message", placeholder="Share how you feel...") | |
| with gr.Row(): | |
| send = gr.Button("Send", variant="primary") | |
| regen = gr.Button("🔁 New advice", variant="secondary") | |
| last_emotion = gr.State("neutral") | |
| last_tip = gr.State("") | |
| advice_pool = gr.State({}) | |
| def respond(user_msg, chat_hist, country_choice, save_flag, _emotion, _tip, _pool): | |
| if not user_msg or not user_msg.strip(): | |
| return chat_hist + [[user_msg, "Please share how you feel 🙂"]], _emotion, _tip, _pool | |
| reply, _, emotion, tip, _pool = chat_step( | |
| user_msg, chat_hist, country_choice, bool(save_flag), _pool | |
| ) | |
| return chat_hist + [[user_msg, reply]], emotion, tip, _pool | |
| def new_advice(chat_hist, _emotion, _tip, _pool): | |
| tip, _pool = pick_advice_from_pool(_emotion, _pool, last_tip=_tip) | |
| reply = format_reply(_emotion, tip) | |
| return chat_hist + [[None, reply]], _emotion, tip, _pool | |
| send.click( | |
| respond, | |
| inputs=[msg, chat, country, save_ok, last_emotion, last_tip, advice_pool], | |
| outputs=[chat, last_emotion, last_tip, advice_pool], | |
| ) | |
| msg.submit( | |
| respond, | |
| inputs=[msg, chat, country, save_ok, last_emotion, last_tip, advice_pool], | |
| outputs=[chat, last_emotion, last_tip, advice_pool], | |
| ) | |
| regen.click( | |
| new_advice, | |
| inputs=[chat, last_emotion, last_tip, advice_pool], | |
| outputs=[chat, last_emotion, last_tip, advice_pool], | |
| ) | |
| # ---- Emergency numbers ---- | |
| with gr.Tab("Emergency numbers"): | |
| gr.Markdown("#### 📟 Emergency numbers (English)") | |
| country_view = gr.Dropdown(choices=list(CRISIS_NUMBERS_EN.keys()), value="United States", label="Country") | |
| crisis_info = gr.Markdown(value=crisis_block_en("United States")) | |
| def show_crisis_for_country_en(c): return crisis_block_en(c) | |
| country_view.change(show_crisis_for_country_en, inputs=country_view, outputs=crisis_info) | |
| # ---- Breathing ---- | |
| with gr.Tab("Breathing"): | |
| gr.Markdown("#### 🌬️ Guided breathing") | |
| with gr.Row(): | |
| pattern = gr.Dropdown( | |
| choices=["4-7-8", "Box (4-4-4-4)", "Coherent (5-5, ~6 breaths/min)"], | |
| value="4-7-8", label="Pattern") | |
| cycles = gr.Slider(1, 10, value=4, step=1, label="Number of cycles") | |
| start_btn = gr.Button("Start", variant="primary") | |
| breathe_out = gr.Markdown() | |
| def _steps_for(p): | |
| if p == "4-7-8": return [("Inhale", 4), ("Hold", 7), ("Exhale", 8)] | |
| elif p.startswith("Box"): return [("Inhale", 4), ("Hold", 4), ("Exhale", 4), ("Hold", 4)] | |
| else: return [("Inhale", 5), ("Exhale", 5)] | |
| def run_breathing(pat, n): | |
| steps = _steps_for(pat) | |
| yield "Starting in 3…"; time.sleep(1) | |
| yield "Starting in 2…"; time.sleep(1) | |
| yield "Starting in 1…"; time.sleep(1) | |
| for c in range(1, int(n) + 1): | |
| for label, secs in steps: | |
| for t in range(secs, 0, -1): | |
| dots = "•" * (secs - t + 1) | |
| yield f"**Cycle {c}/{int(n)}** \n**{label}** — {t}s \n{dots}" | |
| time.sleep(1) | |
| yield "✅ Done. Notice how your body feels." | |
| start_btn.click(run_breathing, inputs=[pattern, cycles], outputs=[breathe_out]) | |
| # ---- Journal (simple) ---- | |
| with gr.Tab("Journal"): | |
| gr.Markdown("#### 📝 Journal — write, save, export all") | |
| with gr.Row(): | |
| j_title = gr.Textbox(label="Title (optional)") | |
| j_emotion = gr.Dropdown( | |
| choices=["neutral","sadness","fear","anger","nervousness","boredom","grief","love","joy","curiosity","gratitude"], | |
| value="neutral", label="Emotion" | |
| ) | |
| j_text = gr.Textbox(lines=10, label="Your entry", placeholder="Write whatever you want to remember...") | |
| with gr.Row(): | |
| j_save = gr.Button("Save entry", variant="primary") | |
| j_clear = gr.Button("Clear") | |
| j_status = gr.Markdown() | |
| gr.Markdown("##### Your entries") | |
| with gr.Row(): | |
| j_search = gr.Textbox(label="Search", placeholder="keyword, emotion, title") | |
| j_refresh = gr.Button("Refresh") | |
| j_entries = gr.Dropdown(label="Entries (newest first)", choices=[], value=None) | |
| j_table = gr.Dataframe(headers=["UTC time","Emotion","Title","Preview"], value=[], interactive=False) | |
| j_view = gr.Markdown() | |
| # export all | |
| j_export_all_btn = gr.Button("⬇️ Export ALL entries (zip)") | |
| j_export_all_file = gr.File(label="Download zip", visible=True) | |
| def _refresh_entries(search): | |
| options, table = journal_list(search or "", 50) | |
| return gr.Dropdown(choices=options, value=None), table | |
| def _save_entry(title, text, emo, search): | |
| ok, msg = journal_save(title, text, emo) | |
| drop, table = _refresh_entries(search) | |
| clear_text = "" if ok else text | |
| clear_title = "" if ok else title | |
| return msg, drop, table, clear_text, clear_title | |
| def _load_entry(entry_id): | |
| if entry_id is None: | |
| return "Select an entry to view it here." | |
| data = journal_get(entry_id) | |
| if not data: | |
| return "Entry not found." | |
| title_line = f"### {data['title']}" if data['title'] else "### (Untitled)" | |
| emo_line = f"**Emotion:** {data['emotion'] or '—'} \n**Saved (UTC):** {data['ts']}" | |
| return f"{title_line}\n\n{emo_line}\n\n---\n\n{data['content']}" | |
| def _export_all(): | |
| path, msg = journal_export_all_zip() | |
| return path, msg | |
| j_save.click(_save_entry, inputs=[j_title, j_text, j_emotion, j_search], | |
| outputs=[j_status, j_entries, j_table, j_text, j_title]) | |
| j_clear.click(lambda: ("",), outputs=[j_text]) | |
| j_refresh.click(_refresh_entries, inputs=[j_search], outputs=[j_entries, j_table]) | |
| j_search.submit(_refresh_entries, inputs=[j_search], outputs=[j_entries, j_table]) | |
| j_entries.change(_load_entry, inputs=[j_entries], outputs=[j_view]) | |
| j_export_all_btn.click(_export_all, outputs=[j_export_all_file, j_status]) | |
| if __name__ == "__main__": | |
| demo.queue() | |
| demo.launch() | |