From 47b6562f4822e557d066cf5a2cdcc78105da1333 Mon Sep 17 00:00:00 2001 From: jae Date: Sun, 19 Apr 2026 14:05:09 +0000 Subject: [PATCH] feat: JAE AI chat memory system (Memoria-style) --- api/app.py | 121 +++++++++++++++- api/data/changelog.json | 136 ++++++++++-------- css/chat-memory.css | 303 ++++++++++++++++++++++++++++++++++++++++ index.html | 2 + js/chat-memory.js | 272 ++++++++++++++++++++++++++++++++++++ js/chat.js | 245 +++++++++++++++++++++++++++++--- 6 files changed, 1001 insertions(+), 78 deletions(-) create mode 100644 css/chat-memory.css create mode 100644 js/chat-memory.js diff --git a/api/app.py b/api/app.py index 0726694..1adfad0 100644 --- a/api/app.py +++ b/api/app.py @@ -706,10 +706,15 @@ def venice_chat(): data = request.get_json(force=True, silent=True) or {} user_msg = data.get('message', '').strip() history = data.get('history', []) + memory_context = (data.get('memory_context') or '').strip() if not user_msg: return jsonify({'error': 'Empty message'}), 400 - messages = [{'role': 'system', 'content': JAE_SYSTEM_PROMPT}] + system_content = JAE_SYSTEM_PROMPT + if memory_context: + system_content = memory_context + '\n\n' + JAE_SYSTEM_PROMPT + + messages = [{'role': 'system', 'content': system_content}] for h in history[-20:]: # Keep last 20 exchanges max messages.append({'role': h.get('role', 'user'), 'content': h.get('content', '')}) messages.append({'role': 'user', 'content': user_msg}) @@ -740,6 +745,120 @@ def venice_chat(): except Exception as e: return jsonify({'error': str(e)}), 500 +# ─── Chat Memory Extraction ────────────────────────── +@app.route('/api/chat/extract-memories', methods=['POST']) +def extract_memories(): + """Extract key facts about the user from recent chat turns. + Returns JSON array of {text, category, importance}. Returns [] on any error.""" + try: + keys = load_json('apikeys.json') + venice_key = keys.get('venice', {}).get('api_key', '') + # Prefer gemma-4-uncensored for extraction; fall back to configured chat model + venice_model = keys.get('venice', {}).get('memory_model') or 'gemma-4-uncensored' or keys.get('venice', {}).get('model', 'llama-3.3-70b') + if not venice_key: + return jsonify([]) + + data = request.get_json(force=True, silent=True) or {} + msgs = data.get('messages', []) or [] + if not isinstance(msgs, list) or len(msgs) == 0: + return jsonify([]) + + # Build a compact transcript for the extractor + transcript_lines = [] + for m in msgs[-12:]: + role = (m.get('role') or 'user').upper() + content = (m.get('content') or '').strip() + if content: + transcript_lines.append(f'{role}: {content}') + transcript = '\n'.join(transcript_lines) + if not transcript: + return jsonify([]) + + system_prompt = ( + 'You extract key persistent facts about the USER from a short conversation transcript. ' + 'Output ONLY a valid JSON array (no prose, no markdown code fences). ' + 'Each item must be an object: {"text": string, "category": one of "identity"|"preference"|"project"|"skill"|"goal"|"relationship"|"other", "importance": number 0.0-1.0}. ' + 'Focus on: identity (name, location, job), preferences (likes, dislikes, style), projects (current work), ' + 'skills, goals, and relationships (people mentioned). ' + 'Ignore ephemeral chat (greetings, questions, assistant responses). ' + 'If no new persistent facts, return [].' + ) + + extraction_messages = [ + {'role': 'system', 'content': system_prompt}, + {'role': 'user', 'content': 'Transcript:\n\n' + transcript + '\n\nReturn ONLY the JSON array.'} + ] + + resp = req.post( + 'https://api.venice.ai/api/v1/chat/completions', + headers={ + 'Authorization': f'Bearer {venice_key}', + 'Content-Type': 'application/json' + }, + json={ + 'model': venice_model, + 'messages': extraction_messages, + 'max_tokens': 1024, + 'temperature': 0.2 + }, + timeout=45 + ) + if not resp.ok: + return jsonify([]) + result = resp.json() + raw = (result.get('choices', [{}])[0].get('message', {}).get('content') or '').strip() + if not raw: + raw = (result.get('choices', [{}])[0].get('message', {}).get('reasoning_content') or '').strip() + if not raw: + return jsonify([]) + + # Strip possible markdown code fences + import re as _re, json as _json + fenced = _re.search(r'```(?:json)?\s*(\[[\s\S]*?\])\s*```', raw) + if fenced: + raw = fenced.group(1) + else: + # Try to isolate the first JSON array in the text + match = _re.search(r'(\[[\s\S]*\])', raw) + if match: + raw = match.group(1) + + try: + parsed = _json.loads(raw) + except Exception: + return jsonify([]) + + if not isinstance(parsed, list): + return jsonify([]) + + allowed_cats = {'identity', 'preference', 'project', 'skill', 'goal', 'relationship', 'other'} + cleaned = [] + for item in parsed: + if not isinstance(item, dict): + continue + text = (item.get('text') or '').strip() + if not text or len(text) > 500: + continue + cat = (item.get('category') or 'other').lower().strip() + if cat not in allowed_cats: + cat = 'other' + try: + imp = float(item.get('importance', 0.5)) + except Exception: + imp = 0.5 + imp = max(0.0, min(1.0, imp)) + cleaned.append({'text': text, 'category': cat, 'importance': imp}) + + return jsonify(cleaned[:20]) + except Exception as e: + # Never break the chat — always return an empty array on failure + try: + app.logger.warning(f'extract_memories error: {e}') + except Exception: + pass + return jsonify([]) + + # ─── Globe Config ──────────────────────────────────── @app.route('/api/globe') def get_globe(): diff --git a/api/data/changelog.json b/api/data/changelog.json index 2586cb9..a65f8ce 100644 --- a/api/data/changelog.json +++ b/api/data/changelog.json @@ -1,11 +1,29 @@ { "site": "jaeswift.xyz", "entries": [ + { + "version": "1.34.0", + "date": "19/04/2026", + "category": "FEATURE", + "title": "JAE AI Memory System (Memoria-style)", + "changes": [ + "Browser-local persistent memory for JAE-AI chat via localStorage (key jae-ai-memory-v1, max 100 entries, auto-pruned by importance)", + "Auto memory extraction every 4 user messages via new /api/chat/extract-memories endpoint (Venice gemma-4-uncensored)", + "Relevance-ranked memory injection: top 5 keyword-matched + top 3 recent memories prepended as [REMEMBERED FACTS] block in system prompt per message", + "Chat history persistence across page reloads (last 50 messages in localStorage key jae-ai-history-v1, auto-restored with RESTORED FROM MEMORY divider)", + "Memory Vault modal UI: military/terminal themed, grouped by category (identity/preference/project/skill/goal/relationship/other) with per-entry delete, importance/timestamp display", + "Modal controls: EXPORT JSON (download backup), FORCE EXTRACT (manual trigger), CLEAR HISTORY (wipes conversation), CLEAR ALL MEMORIES (with confirm)", + "Memory button in chat header with live count badge + privacy info tooltip (ℹ) explaining local-only storage", + "Fuzzy dedup: new memories >80% similar to existing are merged (importance boosted) instead of duplicated", + "Privacy-first design: all memories stay in the user’s browser; only current query + small relevant subset sent to Venice per message", + "Graceful degradation: missing localStorage or extraction failures never break the chat loop" + ] + }, { "version": "1.33.0", "date": "19/04/2026", "category": "FEATURE", - "title": "Homepage SERVER METRICS \u2014 Real Live Data", + "title": "Homepage SERVER METRICS — Real Live Data", "changes": [ "SERVER METRICS panel now shows real VPS metrics via /api/stats", "CPU LOAD bar: real load_avg / nproc percentage", @@ -34,7 +52,7 @@ "Purged file from entire git history via git-filter-repo (all 120 commits rewritten)", "Force-pushed cleaned history to Gitea (old commits garbage-collected)", "Created apikeys.example.json template for future contributors", - "Rotated Venice API key \u2014 old key revoked by user", + "Rotated Venice API key — old key revoked by user", "VPS file permissions hardened: chmod 600 api/data/apikeys.json", "Verified raw URL git.jaeswift.xyz/.../apikeys.json now returns HTTP 404", "Audit confirmed: zero occurrences of any API key in git history across all branches" @@ -44,21 +62,21 @@ "version": "1.31.0", "date": "19/04/2026", "category": "AI", - "title": "AI Model Switch \u2014 Gemma-4-Uncensored", + "title": "AI Model Switch — Gemma-4-Uncensored", "changes": [ "Switched JAE AI chat model from olafangensan-glm-4.7-flash-heretic to gemma-4-uncensored", "Switched SITREP daily briefing generator to same model for consistency", "Updated admin panel chat defaults: model + header tag now reflect GEMMA-4-UNCENSORED", - "API restarted and verified live \u2014 chat now returns tighter, more natural replies" + "API restarted and verified live — chat now returns tighter, more natural replies" ] }, { "version": "v1.30.0", "date": "18/04/2026", - "title": "ARMOURY: Wallet X-Ray \u2014 Solana Wallet Analyser", + "title": "ARMOURY: Wallet X-Ray — Solana Wallet Analyser", "category": "ARMOURY", "changes": [ - "New tool: Wallet X-Ray \u2014 deep scan any Solana wallet address or connect your own wallet", + "New tool: Wallet X-Ray — deep scan any Solana wallet address or connect your own wallet", "Overview panel: SOL balance with live USD value, total portfolio value, wallet age, transaction count, and activity rating", "Token Holdings: full SPL token table with logos, balances, live prices via Jupiter Price API, and USD values sorted by value", "NFT Detection: identifies NFTs (0-decimal single-supply tokens) with image grid and Solscan links", @@ -70,7 +88,7 @@ "URL parameter support: ?address=... for direct wallet scanning via shared links", "Wallet X-Ray card added to LAB page with cyan/turquoise theme", "Military radar sweep loading animation during wallet scan", - "Fully responsive design \u2014 works on mobile and desktop" + "Fully responsive design — works on mobile and desktop" ] }, { @@ -80,14 +98,14 @@ "category": "UNREDACTED", "changes": [ "Added 9 new collections across UFO/UAP, Covert Operations, and Government categories with 15 indexed documents", - "COMETA Report (France, 1999): 2 English translations of landmark French military UFO assessment \u2014 163 pages total", - "RAAF UFO Files (Australia): Declassified Royal Australian Air Force intelligence file \u2014 18 pages", - "Project Magnet (Canada, 1950-54): Official Canadian government UFO research programme documents \u2014 6 pages", - "NZDF UFO/UAP Files (New Zealand, 1984-2024): 3 documents including Cold War sighting reports and modern OIA responses \u2014 136 pages", - "Opera\u00e7\u00e3o Prato (Brazil, 1977): Secret Brazilian Air Force UFO investigation in the Amazon \u2014 58 pages", + "COMETA Report (France, 1999): 2 English translations of landmark French military UFO assessment — 163 pages total", + "RAAF UFO Files (Australia): Declassified Royal Australian Air Force intelligence file — 18 pages", + "Project Magnet (Canada, 1950-54): Official Canadian government UFO research programme documents — 6 pages", + "NZDF UFO/UAP Files (New Zealand, 1984-2024): 3 documents including Cold War sighting reports and modern OIA responses — 136 pages", + "Operação Prato (Brazil, 1977): Secret Brazilian Air Force UFO investigation in the Amazon — 58 pages", "COINTELPRO: 758-page FBI surveillance programme compilation plus analytical examination added to existing collection", - "Operation Paperclip (1945-59): Declassified CIA documents on Nazi scientist recruitment programme \u2014 11 pages", - "Watergate Scandal (1972-74): Ford Presidential Library documents on the constitutional crisis \u2014 35 pages", + "Operation Paperclip (1945-59): Declassified CIA documents on Nazi scientist recruitment programme — 11 pages", + "Watergate Scandal (1972-74): Ford Presidential Library documents on the constitutional crisis — 35 pages", "Iran-Contra Affair (1985-87): Complete 506-page Congressional investigation report", "New countries added to UFO/UAP category: France, Australia, Canada, New Zealand, Brazil" ] @@ -99,12 +117,12 @@ "category": "CRIME SCENE", "changes": [ "Added 6 new crime case collections across cold-cases, serial-killers, and landmark-cases with 11 indexed documents", - "D.B. Cooper Hijacking (1971): FBI investigation files \u2014 162 pages on America's only unsolved aircraft hijacking", - "JonBen\u00e9t Ramsey Murder (1996): Autopsy report, unsealed grand jury indictment, and analytical case study \u2014 19 pages", - "Black Dahlia / Elizabeth Short (1947): Complete FBI investigation file \u2014 204 pages on LA's most famous unsolved murder", - "Delphi Murders (2017): Probable cause affidavit and court documents from the Richard Allen prosecution \u2014 95 pages", - "Harold Shipman (1975-98): First three reports of the Shipman Inquiry (Dame Janet Smith) \u2014 1,162 pages on Britain's worst serial killer", - "Moors Murders \u2014 Brady & Hindley (1963-65): Mental Health Review Tribunal academic paper \u2014 22 pages", + "D.B. Cooper Hijacking (1971): FBI investigation files — 162 pages on America's only unsolved aircraft hijacking", + "JonBenét Ramsey Murder (1996): Autopsy report, unsealed grand jury indictment, and analytical case study — 19 pages", + "Black Dahlia / Elizabeth Short (1947): Complete FBI investigation file — 204 pages on LA's most famous unsolved murder", + "Delphi Murders (2017): Probable cause affidavit and court documents from the Richard Allen prosecution — 95 pages", + "Harold Shipman (1975-98): First three reports of the Shipman Inquiry (Dame Janet Smith) — 1,162 pages on Britain's worst serial killer", + "Moors Murders — Brady & Hindley (1963-65): Mental Health Review Tribunal academic paper — 22 pages", "New landmark-cases/US subcategory with Delphi Murders as first entry", "Total new document pages added: 1,664 across all crime scene collections" ] @@ -112,18 +130,18 @@ { "version": "v1.27.0", "date": "18/04/2026", - "title": "CRIME SCENE: UK Murder Cases \u2014 Mass Upload", + "title": "CRIME SCENE: UK Murder Cases — Mass Upload", "category": "CRIME SCENE", "changes": [ "Added 11 UK murder case collections across 4 categories with 57 indexed documents", "New 'Landmark Cases' category for cases that changed British law and policing", - "Daniel Morgan (1987): 3-volume Independent Panel Report \u2014 1,276 pages on Met Police corruption", + "Daniel Morgan (1987): 3-volume Independent Panel Report — 1,276 pages on Met Police corruption", "Claudia Lawrence (2009): ICO FOI audit of North Yorkshire Police practices", "Jill Dando (1999): Barry George appeal judgment and CCRC referral decision", "Suzy Lamplugh (1986): Suzy Lamplugh Trust safety resources and case documentation", - "Stephen Lawrence (1993): Complete Macpherson Report \u2014 389 pages, coined 'institutional racism'", + "Stephen Lawrence (1993): Complete Macpherson Report — 389 pages, coined 'institutional racism'", "James Bulger (1993): ECHR Grand Chamber judgments, anonymity injunction, minimum term review", - "Damilola Taylor (2000): Sentamu Oversight Panel investigation review \u2014 56 pages", + "Damilola Taylor (2000): Sentamu Oversight Panel investigation review — 56 pages", "Lee Rigby (2013): ISC intelligence report (200 pages), Government response, sentencing remarks", "Lord Lucan (1974): Presumption of Death Act legal analysis for Sandra Rivett murder", "Madeleine McCann (2007): PJ Police Report translation (57 pages), Jane Tanner statements (4 parts)", @@ -138,10 +156,10 @@ "category": "CRIME SCENE", "changes": [ "Populated the Police Reports subcollection for the Zodiac Killer with 5 documents (207 pages, 26.4 MB)", - "Added Lake Herman Road police reports \u2014 Benicia PD & Solano County Sheriff (60 pages, 5.6 MB)", - "Added Blue Rock Springs police reports \u2014 Vallejo PD (75 pages, 10.3 MB)", - "Added Lake Berryessa police reports \u2014 Napa County Sheriff's Office (35 pages, 5.1 MB)", - "Added Presidio Heights / Paul Stine police reports \u2014 SFPD (2 pages, 0.4 MB)", + "Added Lake Herman Road police reports — Benicia PD & Solano County Sheriff (60 pages, 5.6 MB)", + "Added Blue Rock Springs police reports — Vallejo PD (75 pages, 10.3 MB)", + "Added Lake Berryessa police reports — Napa County Sheriff's Office (35 pages, 5.1 MB)", + "Added Presidio Heights / Paul Stine police reports — SFPD (2 pages, 0.4 MB)", "Added California Department of Justice investigation report (35 pages, 5.0 MB)", "Zodiac Killer collection now contains 26 documents across 4 subcollections totalling approximately 78 MB", "All documents sourced from zodiackiller.com's authenticated police report archive" @@ -150,7 +168,7 @@ { "version": "1.25.0", "date": "16/04/2026", - "title": "Changelog Fix \u2014 Date Format & Missing Entries", + "title": "Changelog Fix — Date Format & Missing Entries", "category": "fix", "changes": [ "Fixed NaN/NaN/NaN date display bug in changelog renderer", @@ -167,18 +185,18 @@ "category": "fix", "changes": [ "Replaced encrypted/unreadable MKUltra PDF with two working documents", - "Added CIA Inspector General Report (1963) \u2014 48-page TOP SECRET internal review", - "Added Senate Hearing transcript (1977) \u2014 171-page Congressional testimony exposing 149 sub-projects" + "Added CIA Inspector General Report (1963) — 48-page TOP SECRET internal review", + "Added Senate Hearing transcript (1977) — 171-page Congressional testimony exposing 149 sub-projects" ] }, { "version": "1.23.0", "date": "16/04/2026", - "title": "PROPAGANDA \u2192 UNREDACTED Rename + Nav Animation + CRIME SCENE", + "title": "PROPAGANDA → UNREDACTED Rename + Nav Animation + CRIME SCENE", "category": "feature", "changes": [ "Renamed PROPAGANDA section to UNREDACTED across all pages, nav, API, and URLs", - "Built block-reveal animation on UNREDACTED nav item (\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2192 UNREDACTED \u2192 reverse \u2192 loop)", + "Built block-reveal animation on UNREDACTED nav item (██████████ → UNREDACTED → reverse → loop)", "Added global document search across all UNREDACTED categories, titles, and descriptions", "Built new CRIME SCENE section at /depot/crimescene with red CRT theme", "Four crime categories: Unsolved Murders, Serial Killers, Court Transcripts, Cold Cases", @@ -189,12 +207,12 @@ { "version": "1.22.0", "date": "15/04/2026", - "title": "CRIME SCENE: Zodiac Killer Expanded \u2014 Letters, Ciphers & Subcollections", + "title": "CRIME SCENE: Zodiac Killer Expanded — Letters, Ciphers & Subcollections", "category": "CRIME SCENE", "changes": [ "Fixed PDF path bug: document URLs now correctly include country code (US) via subcollection routing", "Restructured Zodiac Killer into 4 subcollections: FBI Investigation Files, Zodiac Letters & Cards, The Zodiac Ciphers, Police Reports & Crime Scene Documents", - "Added 11 original Zodiac letter PDFs (1969\u20131974) from zodiackiller.com: Chronicle/Examiner cipher letters, Debut letter, Stine bloody shirt letter, Bus Bomb letter, Belli letter, Dragon card, Phillips 66 map letter, Little List/Mikado letter, Exorcist letter, Citizen card", + "Added 11 original Zodiac letter PDFs (1969–1974) from zodiackiller.com: Chronicle/Examiner cipher letters, Debut letter, Stine bloody shirt letter, Bus Bomb letter, Belli letter, Dragon card, Phillips 66 map letter, Little List/Mikado letter, Exorcist letter, Citizen card", "Added 3 cipher PDFs: Z408 three-part cipher (solved 1969), Z340 cipher (solved 2020), Z32 map code cipher (unsolved)", "Added Z340 Solution academic paper by Oranchak, Blake & Van Eycke (2024, 38 pages) from arXiv", "Each of the 21 new documents has a unique historical description with contextual detail", @@ -210,7 +228,7 @@ "category": "CRIME SCENE", "changes": [ "Added complete FBI Zodiac Killer investigation files (6 parts, 1,116 pages, 34MB)", - "Files sourced from FBI Vault via Archive.org \u2014 declassified FOIA release", + "Files sourced from FBI Vault via Archive.org — declassified FOIA release", "Each document includes unique summary describing specific contents", "Covers: Arthur Leigh Allen suspect investigation, cipher analysis, forensic lab reports, fingerprint comparisons, decades of tips and suspect referrals", "PDFs served from /crimescene/docs/serial-killers/US/zodiac-killer/", @@ -224,9 +242,9 @@ "category": "feature", "changes": [ "Added PDF text search to document viewer (Ctrl+F, green/amber highlights, match counter, case toggle)", - "Text layer enabled \u2014 select and copy text from PDFs", + "Text layer enabled — select and copy text from PDFs", "Added unique descriptions to all 113 UK MOD UFO documents sourced from National Archives highlights guides", - "Added Project Condign (250MB, 460pp SECRET UK EYES ONLY) \u2014 the classified DI55 UAP study", + "Added Project Condign (250MB, 460pp SECRET UK EYES ONLY) — the classified DI55 UAP study", "Downloaded US documents: Project Blue Book, CIA UFO Collection, NSA UFO Documents, Pentagon UAP Report", "Downloaded Covert Ops: MKUltra, Stargate Program, Operation Northwoods", "Downloaded Government: JFK Warren Commission, Pentagon Papers, CIA Torture Report", @@ -240,7 +258,7 @@ "category": "fix", "changes": [ "Fixed dispatches post pages crashing (mood type error + fallback path)", - "SOL price ticker fixed \u2014 switched to Binance API (CORS-friendly)", + "SOL price ticker fixed — switched to Binance API (CORS-friendly)", "Tightened navbar spacing between SOL price and wallet connect", "Converted all post mood values from integers to proper strings" ] @@ -251,19 +269,19 @@ "title": "Admin Panel Overhaul", "category": "feature", "changes": [ - "Fixed broken Editor section \u2014 full post editing with live preview", - "Fixed broken Backups section \u2014 export/import site data as ZIP", - "Added SITREP admin section \u2014 generate reports, view archive", - "Added Data Sync section \u2014 trigger Contraband/RECON syncs, view stats", - "Added Changelog admin section \u2014 CRUD for maintenance log entries", - "Added Cron Jobs section \u2014 view/toggle all scheduled tasks", + "Fixed broken Editor section — full post editing with live preview", + "Fixed broken Backups section — export/import site data as ZIP", + "Added SITREP admin section — generate reports, view archive", + "Added Data Sync section — trigger Contraband/RECON syncs, view stats", + "Added Changelog admin section — CRUD for maintenance log entries", + "Added Cron Jobs section — view/toggle all scheduled tasks", "Reorganised sidebar into grouped sections" ] }, { "version": "1.18.0", "date": "06/04/2026", - "title": "SITREP \u2014 Daily AI Briefing System", + "title": "SITREP — Daily AI Briefing System", "category": "feature", "changes": [ "Built automated daily intelligence briefing at /transmissions/sitrep", @@ -277,7 +295,7 @@ { "version": "1.17.0", "date": "06/04/2026", - "title": "TOKEN FORGE \u2014 SPL Token Launcher", + "title": "TOKEN FORGE — SPL Token Launcher", "category": "feature", "changes": [ "Built token launcher at /tokenlauncher with full SPL token creation", @@ -321,7 +339,7 @@ "changes": [ "Global wallet connect button in navbar across all 28 pages", "Multi-wallet support: Phantom, Solflare, Backpack, Coinbase, Trust, MetaMask, Jupiter", - "Persistent connection via localStorage \u2014 survives page navigation", + "Persistent connection via localStorage — survives page navigation", "Connected dropdown with address copy, Solscan link, disconnect", "Global window.solWallet API for all Solana features", "Refactored soldomains.js to use shared wallet (removed 146 lines)" @@ -330,7 +348,7 @@ { "version": "1.13.0", "date": "05/04/2026", - "title": "RADAR \u2014 Live Tech News Feed", + "title": "RADAR — Live Tech News Feed", "category": "feature", "changes": [ "Built live tech news aggregator at /transmissions/radar", @@ -353,7 +371,7 @@ { "version": "1.11.0", "date": "04/04/2026", - "title": "RECON \u2014 Site Restructure & Accordion Navigation", + "title": "RECON — Site Restructure & Accordion Navigation", "category": "feature", "changes": [ "Moved RECON to /depot/recon for consistency with other depot pages", @@ -366,7 +384,7 @@ { "version": "1.10.0", "date": "04/04/2026", - "title": "RECON \u2014 Curated Lists Rebuild", + "title": "RECON — Curated Lists Rebuild", "category": "feature", "changes": [ "Flattened 4-level navigation to 2-level (sector > list > entries)", @@ -378,7 +396,7 @@ { "version": "1.9.0", "date": "04/04/2026", - "title": "RECON \u2014 Curated Lists Database", + "title": "RECON — Curated Lists Database", "category": "feature", "changes": [ "Parsed 660 curated lists into 28 themed sectors", @@ -395,7 +413,7 @@ "category": "feature", "changes": [ "Subcategories now display as 2-column card grid with expandable detail panels", - "Added weekly auto-sync \u2014 resource database updates every Sunday at 03:00", + "Added weekly auto-sync — resource database updates every Sunday at 03:00", "Click any subcategory card to expand/collapse its entries below", "Active card highlighting with amber glow", "Responsive grid: 2-col desktop, 1-col mobile" @@ -407,8 +425,8 @@ "title": "Sitewide Visual Overhaul", "category": "fix", "changes": [ - "Bumped 64 font sizes sitewide \u2014 no more microscopic text", - "Brightened all text colours: primary #c0c0c0\u2192#d8d8d8, secondary #707070\u2192#999999, muted #3a3a3a\u2192#666666", + "Bumped 64 font sizes sitewide — no more microscopic text", + "Brightened all text colours: primary #c0c0c0→#d8d8d8, secondary #707070→#999999, muted #3a3a3a→#666666", "CONTRABAND page: 4-column category grid with responsive breakpoints", "Purged all third-party attribution references from entire codebase" ] @@ -416,13 +434,13 @@ { "version": "1.6.0", "date": "03/04/2026", - "title": "CONTRABAND \u2014 Classified Resource Index", + "title": "CONTRABAND — Classified Resource Index", "category": "feature", "changes": [ "Launched CONTRABAND page at /depot/contraband with 15,800+ indexed assets", "24 categories with military codenames (CRT-001 through CRT-024)", "Full-text search across all entries via API", - "Starred/top-pick filter system with \u2b50 indicators", + "Starred/top-pick filter system with ⭐ indicators", "Collapsible subcategories with item counts", "Flask API endpoints: /api/contraband, /api/contraband/, /api/contraband/search" ] @@ -444,8 +462,8 @@ "title": "Globe & Chat AI Admin Panels", "category": "feature", "changes": [ - "Admin panel: Globe management section \u2014 server location, rotation speed, arc cities, colours", - "Admin panel: Chat AI configuration \u2014 model selection, system prompt, greeting toggle", + "Admin panel: Globe management section — server location, rotation speed, arc cities, colours", + "Admin panel: Chat AI configuration — model selection, system prompt, greeting toggle", "New API endpoints: /api/globe, /api/chat-config with auth-protected GET/POST", "Interactive colour picker and slider controls for globe parameters", "Arc cities table with add/remove functionality" diff --git a/css/chat-memory.css b/css/chat-memory.css new file mode 100644 index 0000000..110db49 --- /dev/null +++ b/css/chat-memory.css @@ -0,0 +1,303 @@ +/* =================================================== + JAESWIFT.XYZ — JAE-AI Memory System UI + Military/terminal theme for memory vault modal + =================================================== */ + +/* ── Memory button + info icon in chat header ── */ +.chat-mem-btn { + background: transparent; + border: 1px solid rgba(0, 220, 50, 0.4); + color: #00cc33; + font-family: 'JetBrains Mono', 'Courier New', monospace; + font-size: 11px; + padding: 2px 8px; + margin-left: 8px; + cursor: pointer; + letter-spacing: 1px; + text-transform: uppercase; + transition: all 0.15s ease; + display: inline-flex; + align-items: center; + gap: 4px; +} +.chat-mem-btn:hover { + background: rgba(0, 220, 50, 0.1); + border-color: #00cc33; + box-shadow: 0 0 8px rgba(0, 220, 50, 0.3); +} +.chat-mem-count { + color: #9cffc1; + font-weight: 700; +} +.chat-mem-info { + color: rgba(0, 220, 50, 0.55); + font-size: 13px; + margin-left: 8px; + cursor: help; + text-decoration: none; + font-family: 'JetBrains Mono', 'Courier New', monospace; +} +.chat-mem-info:hover { + color: #00cc33; +} + +/* ── Restore divider in chat ── */ +.chat-divider { + text-align: center; + font-family: 'JetBrains Mono', 'Courier New', monospace; + font-size: 10px; + color: rgba(0, 220, 50, 0.4); + letter-spacing: 2px; + margin: 10px 0 14px; + position: relative; +} +.chat-divider::before, +.chat-divider::after { + content: ''; + position: absolute; + top: 50%; + width: 30%; + height: 1px; + background: rgba(0, 220, 50, 0.2); +} +.chat-divider::before { left: 5%; } +.chat-divider::after { right: 5%; } +.chat-divider span { + background: transparent; + padding: 0 8px; +} + +/* ── Memory vault modal ── */ +.mem-modal { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.88); + z-index: 99999; + display: flex; + align-items: center; + justify-content: center; + font-family: 'JetBrains Mono', 'Courier New', monospace; + backdrop-filter: blur(6px); + animation: memFadeIn 0.18s ease; +} +@keyframes memFadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.mem-modal-inner { + background: #050805; + border: 1px solid #00cc33; + box-shadow: 0 0 32px rgba(0, 220, 50, 0.25), inset 0 0 24px rgba(0, 220, 50, 0.04); + width: min(720px, 94vw); + max-height: 88vh; + display: flex; + flex-direction: column; + color: #9cffc1; + animation: memSlideUp 0.25s ease; +} +@keyframes memSlideUp { + from { transform: translateY(14px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +.mem-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 14px; + border-bottom: 1px solid rgba(0, 220, 50, 0.3); + background: linear-gradient(90deg, rgba(0, 220, 50, 0.08), transparent); +} +.mem-modal-title { + color: #00ff44; + font-size: 13px; + letter-spacing: 2px; + text-transform: uppercase; + font-weight: 700; + text-shadow: 0 0 6px rgba(0, 220, 50, 0.5); +} +.mem-close { + background: transparent; + border: 1px solid rgba(255, 80, 80, 0.4); + color: #ff7070; + padding: 3px 10px; + font-size: 10px; + letter-spacing: 1px; + cursor: pointer; + font-family: inherit; + text-transform: uppercase; + transition: all 0.15s ease; +} +.mem-close:hover { + background: rgba(255, 80, 80, 0.12); + border-color: #ff7070; + box-shadow: 0 0 8px rgba(255, 80, 80, 0.3); +} + +.mem-modal-subtitle { + padding: 6px 14px; + font-size: 10px; + color: rgba(156, 255, 193, 0.5); + letter-spacing: 1.5px; + border-bottom: 1px dashed rgba(0, 220, 50, 0.18); + background: rgba(0, 220, 50, 0.02); +} + +.mem-modal-body { + flex: 1; + overflow-y: auto; + padding: 12px 14px; + font-size: 12px; + scrollbar-width: thin; + scrollbar-color: rgba(0, 220, 50, 0.4) transparent; +} +.mem-modal-body::-webkit-scrollbar { width: 6px; } +.mem-modal-body::-webkit-scrollbar-thumb { background: rgba(0, 220, 50, 0.35); } +.mem-modal-body::-webkit-scrollbar-track { background: transparent; } + +.mem-empty { + text-align: center; + padding: 40px 10px; + color: rgba(156, 255, 193, 0.45); + font-size: 11px; + letter-spacing: 2px; + border: 1px dashed rgba(0, 220, 50, 0.2); +} + +.mem-group { + margin-bottom: 16px; +} +.mem-group-title { + color: #00ff44; + font-size: 11px; + letter-spacing: 2px; + padding: 4px 0 6px; + border-bottom: 1px solid rgba(0, 220, 50, 0.25); + margin-bottom: 6px; + text-transform: uppercase; + font-weight: 700; +} +.mem-count { + color: rgba(156, 255, 193, 0.5); + font-weight: 400; + font-size: 10px; + letter-spacing: 1px; +} + +.mem-item { + position: relative; + border-left: 2px solid rgba(0, 220, 50, 0.35); + background: rgba(0, 220, 50, 0.03); + padding: 8px 36px 8px 10px; + margin-bottom: 6px; + transition: all 0.12s ease; +} +.mem-item:hover { + background: rgba(0, 220, 50, 0.07); + border-left-color: #00cc33; +} +.mem-item-text { + color: #c5ffd6; + font-size: 12px; + line-height: 1.4; + word-wrap: break-word; +} +.mem-item-meta { + color: rgba(156, 255, 193, 0.4); + font-size: 9px; + letter-spacing: 1px; + margin-top: 3px; + text-transform: uppercase; +} +.mem-del { + position: absolute; + top: 6px; + right: 6px; + background: transparent; + border: 1px solid rgba(255, 80, 80, 0.3); + color: #ff7070; + width: 22px; + height: 22px; + line-height: 1; + cursor: pointer; + font-family: inherit; + font-size: 14px; + padding: 0; + transition: all 0.12s ease; +} +.mem-del:hover { + background: rgba(255, 80, 80, 0.15); + border-color: #ff7070; + box-shadow: 0 0 6px rgba(255, 80, 80, 0.35); +} + +.mem-modal-footer { + display: flex; + gap: 8px; + padding: 10px 14px; + border-top: 1px solid rgba(0, 220, 50, 0.3); + background: rgba(0, 220, 50, 0.03); + flex-wrap: wrap; + justify-content: flex-end; +} +.mem-btn { + background: transparent; + border: 1px solid rgba(0, 220, 50, 0.4); + color: #9cffc1; + padding: 5px 10px; + font-family: inherit; + font-size: 10px; + letter-spacing: 1.2px; + text-transform: uppercase; + cursor: pointer; + transition: all 0.15s ease; +} +.mem-btn:hover:not(:disabled) { + background: rgba(0, 220, 50, 0.08); + border-color: #00cc33; + color: #00ff44; + box-shadow: 0 0 8px rgba(0, 220, 50, 0.25); +} +.mem-btn:disabled { + opacity: 0.5; + cursor: wait; +} +.mem-btn-clear, +.mem-btn-clearhist { + border-color: rgba(255, 180, 80, 0.45); + color: #ffcf8a; +} +.mem-btn-clear:hover:not(:disabled), +.mem-btn-clearhist:hover:not(:disabled) { + background: rgba(255, 180, 80, 0.1); + border-color: #ffb74d; + color: #ffd28a; + box-shadow: 0 0 8px rgba(255, 180, 80, 0.3); +} +.mem-btn-clear { + border-color: rgba(255, 80, 80, 0.45); + color: #ff9b9b; +} +.mem-btn-clear:hover:not(:disabled) { + background: rgba(255, 80, 80, 0.12); + border-color: #ff7070; + color: #ffbaba; + box-shadow: 0 0 8px rgba(255, 80, 80, 0.35); +} + +/* ── Mobile adjustments ── */ +@media (max-width: 540px) { + .mem-modal-inner { + width: 98vw; + max-height: 94vh; + } + .mem-modal-footer { + justify-content: stretch; + } + .mem-btn { + flex: 1 1 45%; + font-size: 9px; + padding: 6px 4px; + } +} diff --git a/index.html b/index.html index 264fb0e..87fb140 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,7 @@ JAESWIFT // SYSTEMS ONLINE + @@ -584,6 +585,7 @@ + diff --git a/js/chat-memory.js b/js/chat-memory.js new file mode 100644 index 0000000..54737d1 --- /dev/null +++ b/js/chat-memory.js @@ -0,0 +1,272 @@ +/* =================================================== + JAESWIFT.XYZ — JAE-AI Memory System (Memoria-style) + Browser-local persistent memory for chat continuity + All data stays in localStorage — never leaves device + except as context in the next user message. + =================================================== */ + +(function (global) { + 'use strict'; + + const MEM_KEY = 'jae-ai-memory-v1'; + const HIST_KEY = 'jae-ai-history-v1'; + const MAX_MEMORIES = 100; + const MAX_HISTORY = 50; + const EXTRACT_EVERY_N_USER_MSGS = 4; + + // ─── Storage-safe wrappers ─── + function lsGet(key, fallback) { + try { + const raw = localStorage.getItem(key); + if (!raw) return fallback; + return JSON.parse(raw); + } catch (e) { + console.warn('[chat-memory] storage read fail', e); + return fallback; + } + } + + function lsSet(key, val) { + try { + localStorage.setItem(key, JSON.stringify(val)); + return true; + } catch (e) { + console.warn('[chat-memory] storage write fail', e); + return false; + } + } + + // ─── ID helper ─── + function uid() { + return 'm_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8); + } + + // ─── Memory ops ─── + function getAll() { + return lsGet(MEM_KEY, []); + } + + function saveAll(list) { + return lsSet(MEM_KEY, list); + } + + function prune(list) { + if (list.length <= MAX_MEMORIES) return list; + // sort by importance asc, then timestamp asc (oldest low importance first to drop) + const sorted = [...list].sort(function (a, b) { + if (a.importance !== b.importance) return a.importance - b.importance; + return (a.timestamp || 0) - (b.timestamp || 0); + }); + const toDrop = list.length - MAX_MEMORIES; + const dropIds = new Set(sorted.slice(0, toDrop).map(function (m) { return m.id; })); + return list.filter(function (m) { return !dropIds.has(m.id); }); + } + + // Simple normalized tokens for fuzzy matching + function tokens(str) { + return (str || '') + .toLowerCase() + .replace(/[^a-z0-9\s]/g, ' ') + .split(/\s+/) + .filter(function (t) { return t.length > 2; }); + } + + // Jaccard similarity of token sets + function similarity(a, b) { + const ta = new Set(tokens(a)); + const tb = new Set(tokens(b)); + if (ta.size === 0 || tb.size === 0) return 0; + let inter = 0; + ta.forEach(function (t) { if (tb.has(t)) inter++; }); + const union = ta.size + tb.size - inter; + return union === 0 ? 0 : inter / union; + } + + function add(text, category, importance) { + text = (text || '').trim(); + if (!text) return null; + category = category || 'other'; + importance = typeof importance === 'number' ? Math.max(0, Math.min(1, importance)) : 0.5; + + let list = getAll(); + + // Dedup: if very similar memory exists, update importance instead of adding + for (let i = 0; i < list.length; i++) { + if (similarity(list[i].text, text) > 0.8) { + list[i].importance = Math.max(list[i].importance || 0, importance); + list[i].timestamp = Date.now(); + saveAll(list); + return list[i]; + } + } + + const mem = { + id: uid(), + text: text, + category: category, + importance: importance, + timestamp: Date.now() + }; + list.push(mem); + list = prune(list); + saveAll(list); + return mem; + } + + function remove(id) { + const list = getAll().filter(function (m) { return m.id !== id; }); + saveAll(list); + } + + function clear() { + saveAll([]); + } + + // ─── Relevance-ranked search ─── + function search(query, limit) { + limit = limit || 5; + const qTokens = tokens(query); + const list = getAll(); + if (qTokens.length === 0 || list.length === 0) return []; + + const scored = list.map(function (m) { + const mTokens = tokens(m.text); + const mSet = new Set(mTokens); + let overlap = 0; + qTokens.forEach(function (t) { if (mSet.has(t)) overlap++; }); + // substring bonus + const lowerText = m.text.toLowerCase(); + const lowerQ = (query || '').toLowerCase(); + let subBonus = 0; + qTokens.forEach(function (t) { + if (lowerText.indexOf(t) !== -1) subBonus += 0.3; + }); + if (lowerQ && lowerText.indexOf(lowerQ) !== -1) subBonus += 1.0; + + const score = overlap + subBonus + (m.importance || 0) * 0.5; + return { mem: m, score: score }; + }); + + scored.sort(function (a, b) { return b.score - a.score; }); + return scored.filter(function (s) { return s.score > 0; }).slice(0, limit).map(function (s) { return s.mem; }); + } + + function recent(limit) { + limit = limit || 3; + return [...getAll()].sort(function (a, b) { return (b.timestamp || 0) - (a.timestamp || 0); }).slice(0, limit); + } + + // ─── Context block for system prompt ─── + function getContextBlock(query) { + const relevant = search(query, 5); + const recentOnes = recent(3); + const seen = new Set(relevant.map(function (m) { return m.id; })); + const combined = relevant.slice(); + recentOnes.forEach(function (m) { + if (!seen.has(m.id)) combined.push(m); + }); + if (combined.length === 0) return ''; + + const lines = combined.map(function (m) { + return '- [' + (m.category || 'other') + '] ' + m.text; + }); + return '[REMEMBERED FACTS ABOUT THIS USER]\n' + lines.join('\n') + '\n[END REMEMBERED FACTS]'; + } + + // ─── Chat history persistence ─── + function getHistory() { + return lsGet(HIST_KEY, []); + } + + function saveHistory(messages) { + const trimmed = messages.slice(-MAX_HISTORY); + return lsSet(HIST_KEY, trimmed); + } + + function appendHistory(msg) { + const h = getHistory(); + h.push(msg); + saveHistory(h); + } + + function clearHistory() { + lsSet(HIST_KEY, []); + } + + // ─── Memory extraction via backend ─── + let _userMsgCounter = 0; + let _extracting = false; + + function incrementUserMsgAndMaybeExtract() { + _userMsgCounter++; + if (_userMsgCounter >= EXTRACT_EVERY_N_USER_MSGS) { + _userMsgCounter = 0; + return extractFromRecentChat(); + } + return Promise.resolve(null); + } + + function extractFromRecentChat() { + if (_extracting) return Promise.resolve(null); + _extracting = true; + + const hist = getHistory().slice(-8); + if (hist.length < 2) { + _extracting = false; + return Promise.resolve(null); + } + + return fetch('/api/chat/extract-memories', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages: hist }) + }) + .then(function (r) { return r.ok ? r.json() : []; }) + .then(function (data) { + const arr = Array.isArray(data) ? data : (data && Array.isArray(data.memories) ? data.memories : []); + const added = []; + arr.forEach(function (item) { + if (item && typeof item.text === 'string' && item.text.trim()) { + const m = add(item.text, item.category || 'other', typeof item.importance === 'number' ? item.importance : 0.5); + if (m) added.push(m); + } + }); + _extracting = false; + return added; + }) + .catch(function (e) { + console.warn('[chat-memory] extraction failed', e); + _extracting = false; + return null; + }); + } + + // ─── Export JSON ─── + function exportJSON() { + const data = { + version: 1, + exported: new Date().toISOString(), + memories: getAll(), + history: getHistory() + }; + return JSON.stringify(data, null, 2); + } + + // ─── Public API ─── + global.chatMemory = { + add: add, + getAll: getAll, + search: search, + remove: remove, + clear: clear, + getContextBlock: getContextBlock, + extractFromRecentChat: extractFromRecentChat, + incrementUserMsgAndMaybeExtract: incrementUserMsgAndMaybeExtract, + getHistory: getHistory, + saveHistory: saveHistory, + appendHistory: appendHistory, + clearHistory: clearHistory, + exportJSON: exportJSON + }; + +})(window); diff --git a/js/chat.js b/js/chat.js index eccf600..8c639ee 100644 --- a/js/chat.js +++ b/js/chat.js @@ -1,6 +1,6 @@ /* =================================================== JAESWIFT.XYZ — JAE-AI Chat Terminal - Venice API chat interface + Venice API chat interface + Memoria-style memory =================================================== */ (function () { @@ -10,15 +10,17 @@ const chatInput = document.getElementById('chatInput'); const chatSend = document.getElementById('chatSend'); const chatStatus = document.getElementById('chatStatus'); + const chatHeader = document.querySelector('.chat-terminal .panel-header'); if (!chatMessages || !chatInput || !chatSend) return; + const mem = window.chatMemory || null; + let history = []; let isWaiting = false; // ─── Render a message bubble ─── function addMessage(role, text) { - // Remove welcome screen on first message const welcome = chatMessages.querySelector('.chat-welcome'); if (welcome) welcome.remove(); @@ -62,7 +64,7 @@ if (el) el.remove(); } - // ─── Typewriter effect for AI responses ─── + // ─── Typewriter effect ─── function typewriterEffect(element, text, speed) { speed = speed || 12; let i = 0; @@ -82,6 +84,44 @@ }); } + // ─── Restore previous chat history from localStorage ─── + function restoreHistory() { + if (!mem) return false; + const saved = mem.getHistory(); + if (!saved || saved.length === 0) return false; + + // Remove welcome and render saved history + const welcome = chatMessages.querySelector('.chat-welcome'); + if (welcome) welcome.remove(); + + saved.forEach(function (m) { + const role = m.role === 'user' ? 'user' : 'assistant'; + const label = document.createElement('span'); + label.className = 'chat-msg-label'; + label.textContent = role === 'user' ? 'YOU' : 'JAE-AI'; + const body = document.createElement('div'); + body.className = 'chat-msg-body'; + body.textContent = m.content || ''; + const el = document.createElement('div'); + el.className = `chat-msg chat-msg-${role}`; + el.appendChild(label); + el.appendChild(body); + chatMessages.appendChild(el); + }); + + // Divider showing restore + const divider = document.createElement('div'); + divider.className = 'chat-divider'; + divider.innerHTML = '— RESTORED FROM MEMORY —'; + chatMessages.appendChild(divider); + + chatMessages.scrollTop = chatMessages.scrollHeight; + + // populate in-memory history + history = saved.slice(); + return true; + } + // ─── Send message to API ─── async function sendMessage() { const text = chatInput.value.trim(); @@ -95,16 +135,21 @@ addMessage('user', text); history.push({ role: 'user', content: text }); + if (mem) mem.appendHistory({ role: 'user', content: text }); showTyping(); + // Build memory context block + const memoryBlock = mem ? mem.getContextBlock(text) : ''; + try { const resp = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text, - history: history.slice(0, -1) + history: history.slice(0, -1), + memory_context: memoryBlock }) }); @@ -124,15 +169,18 @@ const reply = data.reply || 'No response received.'; history.push({ role: 'assistant', content: reply }); + if (mem) mem.appendHistory({ role: 'assistant', content: reply }); - // Keep history manageable - if (history.length > 40) { - history = history.slice(-30); - } + if (history.length > 40) history = history.slice(-30); const bodyEl = addMessage('assistant', ''); await typewriterEffect(bodyEl, reply, 10); + // Trigger memory extraction every N user messages (non-blocking) + if (mem) { + mem.incrementUserMsgAndMaybeExtract().catch(function () { /* silent */ }); + } + } catch (e) { hideTyping(); addMessage('assistant', 'ERROR: Network failure — ' + e.message); @@ -148,6 +196,156 @@ chatInput.focus(); } + // ─── Memory modal ─── + function formatDate(ts) { + try { return new Date(ts).toLocaleString('en-GB'); } catch (e) { return ''; } + } + + function renderMemoryModal() { + if (!mem) return; + const existing = document.getElementById('memModal'); + if (existing) { existing.remove(); return; } + + const modal = document.createElement('div'); + modal.id = 'memModal'; + modal.className = 'mem-modal'; + + const all = mem.getAll(); + const grouped = {}; + all.forEach(function (m) { + const c = m.category || 'other'; + if (!grouped[c]) grouped[c] = []; + grouped[c].push(m); + }); + + const categories = ['identity', 'preference', 'project', 'skill', 'goal', 'relationship', 'other']; + const order = categories.filter(function (c) { return grouped[c]; }) + .concat(Object.keys(grouped).filter(function (c) { return categories.indexOf(c) === -1; })); + + let bodyHTML = ''; + if (all.length === 0) { + bodyHTML = '
NO MEMORIES STORED // CHAT TO BUILD PROFILE
'; + } else { + order.forEach(function (cat) { + bodyHTML += '
' + cat.toUpperCase() + ' (' + grouped[cat].length + ')
'; + grouped[cat].sort(function (a, b) { return (b.timestamp || 0) - (a.timestamp || 0); }).forEach(function (m) { + bodyHTML += '
' + + '
' + escapeHtml(m.text) + '
' + + '
imp: ' + (m.importance || 0).toFixed(2) + ' // ' + formatDate(m.timestamp) + '
' + + '' + + '
'; + }); + bodyHTML += '
'; + }); + } + + modal.innerHTML = ` +
+
+ 🧠 JAE-AI MEMORY VAULT + +
+
STORED LOCALLY // ${all.length} MEMORIES // NEVER LEAVES DEVICE
+
${bodyHTML}
+ +
+ `; + + document.body.appendChild(modal); + + document.getElementById('memClose').addEventListener('click', function () { modal.remove(); }); + modal.addEventListener('click', function (e) { + if (e.target === modal) modal.remove(); + }); + + modal.querySelectorAll('.mem-del').forEach(function (btn) { + btn.addEventListener('click', function () { + const id = btn.getAttribute('data-id'); + mem.remove(id); + renderMemoryModal(); renderMemoryModal(); // toggle off/on to refresh + }); + }); + + document.getElementById('memExport').addEventListener('click', function () { + const blob = new Blob([mem.exportJSON()], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'jae-ai-memory-' + Date.now() + '.json'; + a.click(); + setTimeout(function () { URL.revokeObjectURL(url); }, 1000); + }); + + document.getElementById('memExtract').addEventListener('click', function () { + const btn = document.getElementById('memExtract'); + btn.disabled = true; + btn.textContent = '⌛ EXTRACTING...'; + mem.extractFromRecentChat().then(function () { + renderMemoryModal(); renderMemoryModal(); + }).catch(function () { + btn.textContent = '✖ EXTRACT FAILED'; + }); + }); + + document.getElementById('memClearHist').addEventListener('click', function () { + if (confirm('Clear chat history? Memories will be kept.')) { + mem.clearHistory(); + alert('Chat history cleared. Reload the page.'); + } + }); + + document.getElementById('memClear').addEventListener('click', function () { + if (confirm('PERMANENTLY DELETE ALL MEMORIES? This cannot be undone.')) { + mem.clear(); + renderMemoryModal(); renderMemoryModal(); + } + }); + } + + function escapeHtml(s) { + return String(s || '').replace(/[&<>"']/g, function (c) { + return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]; + }); + } + + // ─── Inject memory button + privacy info into chat header ─── + function injectMemoryUI() { + if (!chatHeader) return; + if (document.getElementById('memBtn')) return; + + const btn = document.createElement('button'); + btn.id = 'memBtn'; + btn.className = 'chat-mem-btn'; + btn.title = 'View / manage local memories'; + btn.innerHTML = '🧠 0'; + btn.addEventListener('click', renderMemoryModal); + + const info = document.createElement('a'); + info.href = '#'; + info.className = 'chat-mem-info'; + info.title = 'All memories stored locally in your browser. Nothing leaves your device except current message context.'; + info.textContent = 'ℹ'; + info.addEventListener('click', function (e) { + e.preventDefault(); + alert('PRIVACY NOTICE\n\nAll JAE-AI memories are stored locally in your browser (localStorage). They never leave your device.\n\nPer message, only the current text plus a small set of relevant memories is sent as context to Venice AI to provide continuity.\n\nYou can view, export, or clear all memories via the 🧠 button.'); + }); + + chatHeader.appendChild(info); + chatHeader.appendChild(btn); + + updateMemCount(); + } + + function updateMemCount() { + const el = document.getElementById('memBtnCount'); + if (el && mem) el.textContent = mem.getAll().length; + } + // ─── Event listeners ─── chatSend.addEventListener('click', sendMessage); chatInput.addEventListener('keydown', function (e) { @@ -157,16 +355,27 @@ } }); - // ─── Auto-greeting after short delay ─── - setTimeout(function () { - showTyping(); + // ─── Init ─── + injectMemoryUI(); + + // Update memory count periodically + setInterval(updateMemCount, 5000); + + const restored = restoreHistory(); + + if (!restored) { + // Auto-greeting after short delay (only if no history restored) setTimeout(function () { - hideTyping(); - var greeting = 'Welcome to JAESWIFT.XYZ — I\'m JAE-AI, your onboard guide. Ask me anything about this system, or try saying "what can I explore here?"'; - history.push({ role: 'assistant', content: greeting }); - var bodyEl = addMessage('assistant', ''); - typewriterEffect(bodyEl, greeting, 15); - }, 1500); - }, 2000); + showTyping(); + setTimeout(function () { + hideTyping(); + var greeting = 'Welcome to JAESWIFT.XYZ — I\'m JAE-AI, your onboard guide. Ask me anything about this system, or try saying "what can I explore here?"'; + history.push({ role: 'assistant', content: greeting }); + if (mem) mem.appendHistory({ role: 'assistant', content: greeting }); + var bodyEl = addMessage('assistant', ''); + typewriterEffect(bodyEl, greeting, 15); + }, 1500); + }, 2000); + } })();