diff mrjunejune/src/jrpg/jrpg.js @ 265:056790c4fb0d

add role-aware Epi assistant prompts Add verified June knowledge, guest/member/admin Copilot profiles, profile-isolated session recovery, animated Epi greetings, and a single authoritative runtime config workflow for inference. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 10:50:30 -0700
parents 04fee26ecce0
children 41a49c29a28f
line wrap: on
line diff
--- a/mrjunejune/src/jrpg/jrpg.js	Fri Aug 07 07:34:12 2026 -0700
+++ b/mrjunejune/src/jrpg/jrpg.js	Fri Aug 07 10:50:30 2026 -0700
@@ -89,6 +89,11 @@
 const SCRIPT_LOADS = new Map();
 const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
 const VALID_PANELS = Object.freeze(["resume", "tools", "blog", "conversations"]);
+const ROLE_GREETINGS = Object.freeze({
+  guest: "Hi, my name is Epi. June usually calls me Epi-chan. I’m here to help you navigate his personal website and learn about his work, projects, and writing.",
+  invited: "Hi! I’m Epi, but June usually calls me Epi-chan. Since June invited you, I can be a little more casual. I can help you explore his site, talk about his projects, or brainstorm simple ideas with you.",
+  admin: "Hi June! I’m Epi-chan. I’m here to help you review your personal website, think through your projects, and draft or refine public-facing ideas.",
+});
 const TOOL_ICON_NAMES = Object.freeze({
   "/notes": "repository",
   "/tools/file_converter": "retry",
@@ -114,6 +119,13 @@
   return loading;
 }
 
+function greetingForSession(sessionData) {
+  if (sessionData?.kind !== "user") return ROLE_GREETINGS.guest;
+  return sessionData.role === "admin"
+    ? ROLE_GREETINGS.admin
+    : ROLE_GREETINGS.invited;
+}
+
 class StreamingTextAnimator {
   constructor(render) {
     this._render = render;
@@ -232,9 +244,8 @@
     this._previousButton = this.querySelector("[data-turn-previous]");
     this._nextButton = this.querySelector("[data-turn-next]");
     this._position = this.querySelector("[data-turn-position]");
-    this._initialMessages = [...(this._messages?.children || [])].map(
-      item => item.cloneNode(true),
-    );
+    this._greetingText = ROLE_GREETINGS.guest;
+    this._greetingAnimator = null;
     this._turnSequence = 0;
     this._activeGroup = null;
     this._onNavigationClick = event => {
@@ -254,6 +265,10 @@
   appendMessage(speaker, text) {
     let group = this._activeGroup || "start";
     if (speaker === "June") {
+      this._greetingAnimator?.cancel();
+      this._greetingAnimator = null;
+      const greeting = this._messages?.querySelector("[data-greeting]");
+      if (greeting) this.setMessageStreaming(greeting, false);
       this._turnSequence++;
       group = `turn-${this._turnSequence}`;
       this._activeGroup = group;
@@ -261,6 +276,42 @@
     return this._createMessage(speaker, text, group, true);
   }
 
+  setGreeting(text, animate = true) {
+    this._greetingText = typeof text === "string" && text
+      ? text
+      : ROLE_GREETINGS.guest;
+    this._showGreeting(animate);
+  }
+
+  _showGreeting(animate) {
+    if (!this._messages) return;
+    this._greetingAnimator?.cancel();
+    this._greetingAnimator = null;
+    this._messages.replaceChildren();
+    this._turnSequence = 0;
+    this._activeGroup = null;
+    const item = this._createMessage("Epi", "", "start", false);
+    if (!item) return;
+    item.dataset.greeting = "true";
+    this._refreshTurnGroups("start");
+    if (!animate) {
+      this.setMessageText(item, this._greetingText);
+      this.setMessageStreaming(item, false);
+      return;
+    }
+    this.setMessageStreaming(item, true);
+    const animator = new StreamingTextAnimator(value => {
+      this.setMessageText(item, value);
+    });
+    this._greetingAnimator = animator;
+    animator.complete(this._greetingText);
+    void animator.waitForIdle().then(() => {
+      if (this._greetingAnimator !== animator) return;
+      this.setMessageStreaming(item, false);
+      this._greetingAnimator = null;
+    });
+  }
+
   _createMessage(speaker, text, group, select) {
     if (!this._messages) return;
     const item = document.createElement("li");
@@ -279,6 +330,8 @@
 
   replaceMessages(turns) {
     if (!this._messages) return;
+    this._greetingAnimator?.cancel();
+    this._greetingAnimator = null;
     this._messages.replaceChildren();
     this._turnSequence = 0;
     this._activeGroup = null;
@@ -286,10 +339,7 @@
       ["user", "assistant"].includes(turn.role)
     );
     if (!visibleTurns.length) {
-      this._messages.append(
-        ...this._initialMessages.map(item => item.cloneNode(true)),
-      );
-      this._refreshTurnGroups("start");
+      this._showGreeting(true);
       return;
     }
     let group = null;
@@ -2116,6 +2166,7 @@
     popSeq += 1;
     archiveCursor = null;
   };
+  chat?.setGreeting(greetingForSession(_sessionData), true);
 
   /* Disable composer until fully initialized */
   composer?.setDisabled(true);
@@ -2301,7 +2352,7 @@
       sessionStorage.removeItem(CONVERSATION_LEGACY_KEY);
       legacyClaimId = null;
       _replaceUrlState(currentPanel, null);
-      chat?.replaceMessages([]);
+      chat?.setGreeting(greetingForSession(_sessionData), true);
       try {
         await refreshArchiveFromScratch();
       } catch (error) {
@@ -2331,7 +2382,7 @@
     sessionStorage.removeItem(CONVERSATION_LEGACY_KEY);
     legacyClaimId = null;
     _replaceUrlState(currentPanel, null);
-    chat?.replaceMessages([]);
+    chat?.setGreeting(greetingForSession(_sessionData), true);
     _renderedIds.clear();
     archiveCursor = null;
     archive?.setConversations([], null);
@@ -2351,7 +2402,7 @@
     sessionStorage.removeItem(CONVERSATION_LEGACY_KEY);
     legacyClaimId = null;
     _replaceUrlState(currentPanel, null);
-    chat?.replaceMessages([]);
+    chat?.setGreeting(ROLE_GREETINGS.guest, true);
     _renderedIds.clear();
     archiveCursor = null;
     archive?.setConversations([], null);
@@ -2432,7 +2483,7 @@
     currentConversationId = null;
     sessionStorage.removeItem(CONVERSATION_STORAGE_KEY);
     _pushUrlState(currentPanel, null);
-    chat?.replaceMessages([]);
+    chat?.setGreeting(greetingForSession(_sessionData), true);
     archive?.setCurrentId(null);
     composer?.querySelector("textarea")?.focus();
   });
@@ -2461,7 +2512,7 @@
             epoch !== principalEpoch) return;
         currentConversationId = null;
         sessionStorage.removeItem(CONVERSATION_STORAGE_KEY);
-        chat?.replaceMessages([]);
+        chat?.setGreeting(greetingForSession(_sessionData), true);
         archive?.setCurrentId(null);
         history.replaceState(
           { panel: popPanel, conversation: null },
@@ -2472,7 +2523,7 @@
     } else if (!popConvId && currentConversationId) {
       currentConversationId = null;
       sessionStorage.removeItem(CONVERSATION_STORAGE_KEY);
-      chat?.replaceMessages([]);
+      chat?.setGreeting(greetingForSession(_sessionData), true);
       archive?.setCurrentId(null);
     }
   };
@@ -2558,7 +2609,7 @@
           currentConversationId = null;
           sessionStorage.removeItem(CONVERSATION_STORAGE_KEY);
           _replaceUrlState(currentPanel, null);
-          chat?.replaceMessages([]);
+          chat?.setGreeting(greetingForSession(_sessionData), true);
         }
         /* Focus surviving element — never the detached trigger */
         requestAnimationFrame(() => {