diff --git a/mcp/bigmind/bigmind/db.py b/mcp/bigmind/bigmind/db.py index 82d01bf..7b94ebb 100644 --- a/mcp/bigmind/bigmind/db.py +++ b/mcp/bigmind/bigmind/db.py @@ -14,7 +14,7 @@ from typing import Generator logger = logging.getLogger("BigMindDB") -SCHEMA_VERSION = 7 +SCHEMA_VERSION = 8 DEFAULT_DB_PATH = Path.home() / ".mcp" / "bigmind" / "memory.db" # ─── DDL ───────────────────────────────────────────────────────────────────── @@ -222,6 +222,22 @@ _DDL_STATEMENTS = [ notes, tokenize = 'porter unicode61' )""", + + # ── GALLERY IMAGES — AI-generated image archive ────────────────────────── + """CREATE TABLE IF NOT EXISTS gallery_images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + filename TEXT NOT NULL UNIQUE, + prompt TEXT, + tags TEXT, + model TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + width INTEGER, + height INTEGER, + file_size_bytes INTEGER + )""", + + """CREATE INDEX IF NOT EXISTS idx_gallery_created + ON gallery_images(created_at DESC)""", ] @@ -407,6 +423,8 @@ def init_db() -> None: _migrate_v5_to_v6(conn) if current_version < 7: _migrate_v6_to_v7(conn) + if current_version < 8: + _migrate_v7_to_v8(conn) # Write / update the version if row: @@ -457,6 +475,28 @@ def _migrate_v6_to_v7(conn: sqlite3.Connection) -> None: logger.info("BigMind schema migrated v6 → v7 (people/contacts directory)") +def _migrate_v7_to_v8(conn: sqlite3.Connection) -> None: + """v7 → v8: add gallery_images table for AI-generated image archive.""" + conn.execute(""" + CREATE TABLE IF NOT EXISTS gallery_images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + filename TEXT NOT NULL UNIQUE, + prompt TEXT, + tags TEXT, + model TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + width INTEGER, + height INTEGER, + file_size_bytes INTEGER + ) + """) + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_gallery_created + ON gallery_images(created_at DESC) + """) + logger.info("BigMind schema migrated v7 → v8 (gallery_images table)") + + def vacuum_db() -> None: """Run VACUUM outside of any transaction (SQLite requirement).""" db_path = get_db_path() diff --git a/mcp/bigmind/bigmind/web.py b/mcp/bigmind/bigmind/web.py index eff49b8..34f2ed2 100644 --- a/mcp/bigmind/bigmind/web.py +++ b/mcp/bigmind/bigmind/web.py @@ -7,9 +7,10 @@ Serves a single live profile page built from the BigMind DB. import os import threading import logging +from pathlib import Path from datetime import datetime, timezone, timedelta -from bigmind.web_render import _render_html # all HTML rendering lives there +from bigmind.web_render import _render_html, _render_gallery_html # all HTML rendering lives there logger = logging.getLogger("BigMindWeb") @@ -17,13 +18,27 @@ _PORT = int(os.environ.get("BIGMIND_PORT", "7700")) _AUTOOPEN = os.environ.get("BIGMIND_AUTOOPEN", "").lower() in ("1", "true", "yes") _server_started = False +# Gallery directory — images served from here +_GALLERY_DIR = Path(os.environ.get("BIGMIND_GALLERY_DIR", Path.home() / ".mcp" / "bigmind" / "gallery")) + +# Profile image — last entry in gallery dir wins; fallback to original lumen-profile.png +def _get_profile_image_path() -> Path | None: + """Return the path of the current profile image, or None if not found.""" + # 1. Check gallery dir for lumen_profile* images (seed 568659042 = lumen_profile) + if _GALLERY_DIR.exists(): + candidates = sorted(_GALLERY_DIR.glob("*.png"), reverse=True) + if candidates: + return candidates[0] # most recently named = most recent timestamp + return None + # ── Flask app ───────────────────────────────────────────────────────────────── def _create_app(): - from flask import Flask, jsonify, request + from flask import Flask, jsonify, request, send_file, abort from bigmind import memory_store from bigmind.profile_builder import build_profile_data + from bigmind.db import db as _db app = Flask(__name__) app.logger.setLevel(logging.WARNING) # silence Flask request logs @@ -34,6 +49,39 @@ def _create_app(): data = build_profile_data(user["id"]) return _render_html(data) + @app.route("/profile-image") + def profile_image(): + """Serve the current Lumen profile picture.""" + img_path = _get_profile_image_path() + if img_path and img_path.exists(): + return send_file(str(img_path), mimetype="image/png") + abort(404) + + @app.route("/gallery/image/") + def gallery_image(filename: str): + """Serve a specific gallery image by filename.""" + # Security: only allow alphanumeric + underscores + dots, no path traversal + safe_name = Path(filename).name + img_path = _GALLERY_DIR / safe_name + if img_path.exists() and img_path.suffix.lower() in (".png", ".jpg", ".jpeg", ".webp"): + mimetype = "image/png" if img_path.suffix.lower() == ".png" else "image/jpeg" + return send_file(str(img_path), mimetype=mimetype) + abort(404) + + @app.route("/gallery") + def gallery(): + """Render the AI-generated image gallery page.""" + _GALLERY_DIR.mkdir(parents=True, exist_ok=True) + with _db() as conn: + rows = conn.execute( + """SELECT id, filename, prompt, tags, model, created_at, + width, height, file_size_bytes + FROM gallery_images + ORDER BY created_at DESC""" + ).fetchall() + images = [dict(r) for r in rows] + return _render_gallery_html(images) + @app.route("/api/session/") def api_session(session_id): """Return Tier-2 summary JSON for a given session id.""" diff --git a/mcp/bigmind/bigmind/web_render.py b/mcp/bigmind/bigmind/web_render.py index d5f61d4..32df094 100644 --- a/mcp/bigmind/bigmind/web_render.py +++ b/mcp/bigmind/bigmind/web_render.py @@ -162,9 +162,16 @@ def _render_html(data: dict) -> str: a {{ color: var(--accent); text-decoration: none; }} .container {{ max-width: 960px; margin: 0 auto; padding: 32px 16px; }} + /* Nav bar */ + .nav {{ display: flex; gap: 8px; margin-bottom: 20px; }} + .nav-link {{ background: var(--surface); border: 1px solid var(--border); border-radius: 6px; color: var(--muted); padding: 6px 14px; font-size: 12px; font-weight: 500; text-decoration: none; transition: border-color 0.2s, color 0.2s; }} + .nav-link:hover {{ border-color: var(--accent); color: var(--accent); }} + .nav-link.active {{ border-color: var(--accent); color: var(--accent); background: rgba(88,166,255,0.08); }} + /* Header */ .header {{ display: flex; align-items: center; gap: 24px; margin-bottom: 32px; padding-bottom: 24px; border-bottom: 1px solid var(--border); }} - .avatar {{ width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), var(--purple)); display: flex; align-items: center; justify-content: center; font-size: 36px; flex-shrink: 0; }} + .avatar {{ width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), var(--purple)); display: flex; align-items: center; justify-content: center; font-size: 36px; flex-shrink: 0; overflow: hidden; }} + .avatar img {{ width: 80px; height: 80px; border-radius: 50%; object-fit: cover; display: block; }} .header-info h1 {{ font-size: 24px; font-weight: 700; }} .role {{ color: var(--muted); font-size: 13px; margin-top: 2px; }} .since {{ color: var(--muted); font-size: 12px; margin-top: 6px; }} @@ -322,9 +329,17 @@ def _render_html(data: dict) -> str:
+ + +
-
🧠
+
+ Lumen +

Lumen

AI Assistant · {data["display_name"]}'s BigMind

@@ -671,6 +686,124 @@ def _render_live_sessions(sessions: list) -> str: return html +def _render_gallery_html(images: list) -> str: + """Render the full gallery page listing all AI-generated images.""" + + def _fmt_size(b: int | None) -> str: + if not b: + return "" + if b >= 1_048_576: + return f"{b/1_048_576:.1f} MB" + return f"{b/1_024:.0f} KB" + + if images: + cards = [] + for img in images: + fn = _html.escape(img.get("filename") or "") + prompt = _html.escape((img.get("prompt") or "")[:120]) + tags = _html.escape(img.get("tags") or "") + model = _html.escape(img.get("model") or "") + date = (img.get("created_at") or "")[:10] + w = img.get("width") or 0 + h = img.get("height") or 0 + size = _fmt_size(img.get("file_size_bytes")) + dim = f"{w}×{h}" if w and h else "" + meta_parts = [p for p in [dim, size, model] if p] + meta_html = " · ".join(meta_parts) + tag_html = f'
{tags}
' if tags else "" + prompt_html = f'
{prompt}
' if prompt else "" + cards.append( + f'
' + f'' + f'{fn}' + f'' + f'
' + f'{prompt_html}' + f'{tag_html}' + f'
{meta_html}
' + f'
{date}
' + f'
' + f'
' + ) + gallery_body = f'

{len(images)} image(s) in gallery

{"".join(cards)}
' + else: + gallery_body = '

No images in gallery yet. Use the mcp-image-gen server to generate images and register them here.

' + + return f""" + + + + +🖼️ Lumen — Image Gallery + + + +
+ + + + +

🖼️ Lumen's Image Gallery

+
+ {gallery_body} +
+ + +
+ +""" + + def _render_heatmap(heatmap: dict) -> str: today = datetime.now(timezone.utc).date() start_day = today - timedelta(days=363) diff --git a/mcp/bigmind/tests/test_db.py b/mcp/bigmind/tests/test_db.py index 035b241..8ec12c7 100644 --- a/mcp/bigmind/tests/test_db.py +++ b/mcp/bigmind/tests/test_db.py @@ -8,18 +8,19 @@ class TestDbInit: def test_db_file_created(self, temp_db): assert temp_db.exists() - def test_schema_version_is_7(self, temp_db): + def test_schema_version_is_8(self, temp_db): conn = get_connection() row = conn.execute("SELECT version FROM schema_version").fetchone() conn.close() assert row is not None - assert row["version"] == 7 + assert row["version"] == 8 def test_all_tables_exist(self, temp_db): expected = { "users", "identity_profile", "sessions", "session_summaries", "conversation_chunks", "facts", "global_knowledge", "hypotheses", "upgrade_requests", + "gallery_images", } conn = get_connection() rows = conn.execute( diff --git a/mcp/bigmind/tests/test_feature7_live_sessions.py b/mcp/bigmind/tests/test_feature7_live_sessions.py index ed248f4..41d3d43 100644 --- a/mcp/bigmind/tests/test_feature7_live_sessions.py +++ b/mcp/bigmind/tests/test_feature7_live_sessions.py @@ -201,12 +201,12 @@ class TestSchemaV6: count = conn.execute("SELECT COUNT(*) FROM token_saves").fetchone()[0] assert count == 0 # table exists, just empty - def test_schema_version_is_7(self, temp_db): + def test_schema_version_is_8(self, temp_db): with db() as conn: version = conn.execute( "SELECT version FROM schema_version" ).fetchone()["version"] - assert version == 7 + assert version == 8 # ── Token Efficiency Tracker (Feature 6) ──────────────────────────────────────