feat: JAE AI chat memory system (Memoria-style)

This commit is contained in:
jae 2026-04-19 14:05:09 +00:00
parent 1da0df7930
commit 47b6562f48
6 changed files with 1001 additions and 78 deletions

View file

@ -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():

View file

@ -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 users 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 (19691974) 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/<slug>, /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"

303
css/chat-memory.css Normal file
View file

@ -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;
}
}

View file

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JAESWIFT // SYSTEMS ONLINE</title>
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/chat-memory.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
@ -584,6 +585,7 @@
<script src="/js/main.js"></script>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script>
<script src="/js/chat-memory.js"></script>
<script src="/js/chat.js"></script>
<script src="/js/globe.js"></script>
<script src="/js/processes.js"></script>

272
js/chat-memory.js Normal file
View file

@ -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);

View file

@ -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 = '<span>— RESTORED FROM MEMORY —</span>';
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 = '<div class="mem-empty">NO MEMORIES STORED // CHAT TO BUILD PROFILE</div>';
} else {
order.forEach(function (cat) {
bodyHTML += '<div class="mem-group"><div class="mem-group-title">' + cat.toUpperCase() + ' <span class="mem-count">(' + grouped[cat].length + ')</span></div>';
grouped[cat].sort(function (a, b) { return (b.timestamp || 0) - (a.timestamp || 0); }).forEach(function (m) {
bodyHTML += '<div class="mem-item" data-id="' + m.id + '">' +
'<div class="mem-item-text">' + escapeHtml(m.text) + '</div>' +
'<div class="mem-item-meta">imp: ' + (m.importance || 0).toFixed(2) + ' // ' + formatDate(m.timestamp) + '</div>' +
'<button class="mem-del" data-id="' + m.id + '" title="Delete">×</button>' +
'</div>';
});
bodyHTML += '</div>';
});
}
modal.innerHTML = `
<div class="mem-modal-inner">
<div class="mem-modal-header">
<span class="mem-modal-title">🧠 JAE-AI MEMORY VAULT</span>
<button class="mem-close" id="memClose">× CLOSE</button>
</div>
<div class="mem-modal-subtitle">STORED LOCALLY // ${all.length} MEMORIES // NEVER LEAVES DEVICE</div>
<div class="mem-modal-body">${bodyHTML}</div>
<div class="mem-modal-footer">
<button class="mem-btn mem-btn-export" id="memExport"> EXPORT JSON</button>
<button class="mem-btn mem-btn-extract" id="memExtract"> FORCE EXTRACT</button>
<button class="mem-btn mem-btn-clearhist" id="memClearHist">🗑 CLEAR HISTORY</button>
<button class="mem-btn mem-btn-clear" id="memClear"> CLEAR ALL MEMORIES</button>
</div>
</div>
`;
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 ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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 = '🧠 <span class="chat-mem-count" id="memBtnCount">0</span>';
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 ───
// ─── 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 () {
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);
}
})();