feat: JAE AI chat memory system (Memoria-style)
This commit is contained in:
parent
1da0df7930
commit
47b6562f48
6 changed files with 1001 additions and 78 deletions
121
api/app.py
121
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():
|
||||
|
|
|
|||
|
|
@ -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/<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
303
css/chat-memory.css
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
272
js/chat-memory.js
Normal 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);
|
||||
245
js/chat.js
245
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 = '<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 ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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 ───
|
||||
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);
|
||||
}
|
||||
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue