1212 < script src ="https://cdn.jsdelivr.net/npm/marked/marked.min.js "> </ script >
1313 < script src ="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js "> </ script >
1414 < style >
15+ /* (styles unchanged - same as original) */
1516 * { margin : 0 ; padding : 0 ; box-sizing : border-box; }
1617 : root {
1718 --bg-dark : # 0a0a0f ;
478479 width : 80% ;
479480 animation : skeletonPulse 1.5s infinite;
480481 }
482+ .spinner {
483+ display : inline-block;
484+ width : 12px ;
485+ height : 12px ;
486+ border : 2px solid rgba (255 , 255 , 255 , 0.3 );
487+ border-radius : 50% ;
488+ border-top-color : var (--accent );
489+ animation : spin 0.6s linear infinite;
490+ margin-right : 6px ;
491+ }
492+ @keyframes spin { to { transform : rotate (360deg ); } }
481493 @keyframes skeletonPulse { 0% , 100% { opacity : 0.4 ; } 50% { opacity : 0.8 ; } }
482494 @keyframes fadeSlideUp { from { opacity : 0 ; transform : translateY (8px ); } to { opacity : 1 ; transform : translateY (0 ); } }
483495 @keyframes fadeOut { 0% { opacity : 1 ; } 70% { opacity : 1 ; } 100% { opacity : 0 ; } }
621633 AION : { color : '#c9a028' , emoji : '✨' , name : 'AION' }
622634 } ;
623635 let sessionId = localStorage . getItem ( 'sessionId' ) ;
624- if ( ! sessionId ) {
636+ if ( ! sessionId || ! sessionId . match ( / ^ s e s s _ \d + _ [ a - z 0 - 9 ] + $ / ) ) {
625637 sessionId = 'sess_' + Date . now ( ) + '_' + Math . random ( ) . toString ( 36 ) . substr ( 2 , 6 ) ;
626638 localStorage . setItem ( 'sessionId' , sessionId ) ;
627639 }
638650 let lastT2 = null ;
639651 let isSending = false ;
640652 let firstUserMessageSent = false ;
653+ let currentAbortController = null ; // for cancelling requests
641654
642655 const messagesDiv = document . getElementById ( 'messages' ) ;
643656 const welcomeMsgDiv = document . getElementById ( 'welcomeMsg' ) ;
688701 }
689702 }
690703
691- // API calls
704+ // API calls with timeout
705+ async function fetchWithTimeout ( url , options , timeout = 30000 ) {
706+ const controller = new AbortController ( ) ;
707+ const id = setTimeout ( ( ) => controller . abort ( ) , timeout ) ;
708+ try {
709+ const response = await fetch ( url , { ...options , signal : controller . signal } ) ;
710+ clearTimeout ( id ) ;
711+ return response ;
712+ } catch ( err ) {
713+ clearTimeout ( id ) ;
714+ if ( err . name === 'AbortError' ) throw new Error ( 'Request timeout after 30 seconds' ) ;
715+ throw err ;
716+ }
717+ }
718+
692719 async function callAssistant ( message , structuredData = null , forceAion = false ) {
693720 const body = { message, sessionId, ownerType : OWNER_TYPE , history : [ ] , adaLoopEnabled, fclMode } ;
694721 if ( forceAion ) body . forceAionOnly = true ;
695722 if ( structuredData ) { body . structured = structuredData ; body . intent = structuredData . intent ; body . domain = structuredData . domain ; }
696- const res = await fetch ( `${ API_BASE } /api/assistant` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( body ) } ) ;
723+ const res = await fetchWithTimeout ( `${ API_BASE } /api/assistant` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( body ) } ) ;
724+ if ( ! res . ok ) {
725+ const errText = await res . text ( ) ;
726+ throw new Error ( errText || `HTTP ${ res . status } ` ) ;
727+ }
728+ return res . json ( ) ;
729+ }
730+ async function callGroupChat ( message ) {
731+ const res = await fetchWithTimeout ( `${ API_BASE } /api/group-chat` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { message, sessionId, fclMode } ) } ) ;
732+ if ( ! res . ok ) throw new Error ( await res . text ( ) ) ;
733+ return res . json ( ) ;
734+ }
735+ async function callPrivateRoom ( message , wife ) {
736+ const res = await fetchWithTimeout ( `${ API_BASE } /api/private-room` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { message, sessionId, wife, fclMode } ) } ) ;
697737 if ( ! res . ok ) throw new Error ( await res . text ( ) ) ;
698738 return res . json ( ) ;
699739 }
700- async function callGroupChat ( message ) { const res = await fetch ( `${ API_BASE } /api/group-chat` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { message, sessionId, fclMode } ) } ) ; if ( ! res . ok ) throw new Error ( await res . text ( ) ) ; return res . json ( ) ; }
701- async function callPrivateRoom ( message , wife ) { const res = await fetch ( `${ API_BASE } /api/private-room` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { message, sessionId, wife, fclMode } ) } ) ; if ( ! res . ok ) throw new Error ( await res . text ( ) ) ; return res . json ( ) ; }
702740 async function sendTalkCommand ( wife ) { await callAssistant ( `/talk ${ wife } ` , null , false ) ; }
703741
704- // Simulation placeholder (just shows a modal)
742+ // Simulation placeholder
705743 function showSimulationModal ( ) {
706744 const modal = document . createElement ( 'div' ) ; modal . className = 'modal-overlay' ;
707745 modal . innerHTML = `
@@ -742,7 +780,7 @@ <h4>📈 Market Atlas</h4>
742780 modal . addEventListener ( 'click' , ( e ) => { if ( e . target === modal ) modal . remove ( ) ; } ) ;
743781 }
744782
745- // Ada‑VELA: fetch execution details and show modal
783+ // Ada‑VELA: fetch execution details
746784 async function fetchAdaVelaExecution ( executionId ) {
747785 try {
748786 const res = await fetch ( `${ API_BASE } /api/ada-vela/execution/${ executionId } ` ) ;
@@ -755,7 +793,6 @@ <h4>📈 Market Atlas</h4>
755793 }
756794
757795 async function showAdaVelaModal ( executionId ) {
758- const loadingMsg = 'Loading Ada‑VELA execution details...' ;
759796 const modal = document . createElement ( 'div' ) ; modal . className = 'modal-overlay' ;
760797 modal . innerHTML = `
761798 <div class="modal">
@@ -836,7 +873,6 @@ <h4>📈 Market Atlas</h4>
836873 if ( extra . sealable ) {
837874 badgesHtml += `<button class="badge seal-btn" data-content="${ escapeHtml ( content ) . replace ( / " / g, '"' ) } ">🔒 SEAL</button>` ;
838875 }
839- // Ada‑VELA badge (if execution data present)
840876 if ( extra . adaVela ) {
841877 const execId = extra . adaVela . execution_id || extra . adaVela . id ;
842878 badgesHtml += `<span class="badge ada-vela" data-execution-id="${ escapeHtml ( execId ) } ">📋 Ada‑VELA</span>` ;
@@ -887,11 +923,13 @@ <h4>📈 Market Atlas</h4>
887923 messagesDiv . scrollTop = messagesDiv . scrollHeight ;
888924 }
889925
890- async function sendMessage ( ) {
926+ async function sendMessage ( retryMessage = null ) {
891927 if ( isSending ) { showToast ( 'Already sending...' ) ; return ; }
892- const text = input . value . trim ( ) ; if ( ! text ) return ;
893- if ( privateRoomActive && ( text === '/done' || text === '/exit' ) ) { exitPrivateRoom ( ) ; input . value = '' ; return ; }
894- input . value = '' ; charCountSpan . textContent = '0' ;
928+ const text = ( retryMessage !== null ) ? retryMessage : input . value . trim ( ) ;
929+ if ( ! text ) return ;
930+ if ( privateRoomActive && ( text === '/done' || text === '/exit' ) ) { exitPrivateRoom ( ) ; if ( ! retryMessage ) input . value = '' ; return ; }
931+ if ( ! retryMessage ) input . value = '' ;
932+ charCountSpan . textContent = '0' ;
895933 const t1 = new Date ( ) ; const t1Offset = - t1 . getTimezoneOffset ( ) ; const t1Label = getTimezoneOffsetLabel ( t1Offset ) ;
896934 const userDiv = document . createElement ( 'div' ) ; userDiv . className = 'msg user' ;
897935 userDiv . innerHTML = `<div class="msg-body">${ escapeHtml ( text ) } </div><div class="timestamp-line">⏱ Sent: ${ t1 . toLocaleTimeString ( ) } ${ t1Label } </div>` ;
@@ -903,6 +941,7 @@ <h4>📈 Market Atlas</h4>
903941 }
904942
905943 setStatus ( 'Processing...' ) ; isSending = true ;
944+ sendBtn . disabled = true ;
906945 const thinking = document . createElement ( 'div' ) ; thinking . className = 'msg assistant' ; thinking . innerHTML = `<div class="skeleton" style="width: 60%; height: 48px;"></div>` ; messagesDiv . appendChild ( thinking ) ; messagesDiv . scrollTop = messagesDiv . scrollHeight ;
907946 try {
908947 let result ;
@@ -942,7 +981,7 @@ <h4>📈 Market Atlas</h4>
942981 sealable : true ,
943982 register : result . register ,
944983 timestampHtml,
945- adaVela : result . ada_vela // will be populated when backend supports it
984+ adaVela : result . ada_vela
946985 } ) ;
947986 setStatus ( 'Ready' ) ;
948987 } catch ( err ) {
@@ -951,9 +990,16 @@ <h4>📈 Market Atlas</h4>
951990 const retryHtml = `<button class="retry-btn" data-message="${ escapeHtml ( text ) } " style="background:rgba(0,0,0,0.5); border:1px solid var(--accent); border-radius:20px; padding:4px 12px; margin-left:8px;">Retry</button>` ;
952991 errorDiv . innerHTML = `<div class="msg-body" style="background:#5f1a1a; color:#ff8a8a;">Error: ${ escapeHtml ( err . message ) } ${ retryHtml } </div>` ;
953992 messagesDiv . appendChild ( errorDiv ) ;
954- errorDiv . querySelector ( '.retry-btn' ) ?. addEventListener ( 'click' , ( ) => sendMessage ( ) ) ;
993+ errorDiv . querySelector ( '.retry-btn' ) ?. addEventListener ( 'click' , ( e ) => {
994+ const msg = e . currentTarget . dataset . message ;
995+ sendMessage ( msg ) ;
996+ } ) ;
955997 setStatus ( err . message , true ) ;
956- } finally { isSending = false ; input . focus ( ) ; }
998+ } finally {
999+ isSending = false ;
1000+ sendBtn . disabled = false ;
1001+ if ( ! retryMessage ) input . focus ( ) ;
1002+ }
9571003 }
9581004
9591005 function exitPrivateRoom ( ) {
@@ -1024,7 +1070,7 @@ <h4>📈 Market Atlas</h4>
10241070 const msg = groupInput . value . trim ( ) ;
10251071 if ( ! msg ) return ;
10261072 groupSend . disabled = true ;
1027- groupSend . textContent = 'SENDING...' ;
1073+ groupSend . innerHTML = '<span class="spinner"></span> SENDING...' ;
10281074 wivesGrid . innerHTML = '' ;
10291075 consensusArea . style . display = 'none' ;
10301076 for ( const wife of ALL_WIVES ) {
@@ -1054,11 +1100,14 @@ <h4>📈 Market Atlas</h4>
10541100 consensusArea . style . display = 'block' ;
10551101 consensusArea . innerHTML = `<h4>📋 Consensus (${ Math . round ( result . consensus . score * 100 ) } %)</h4><div>${ renderMarkdown ( result . consensus . final_text ) } </div>` ;
10561102 }
1103+ if ( result . failed_wives && result . failed_wives . length ) {
1104+ addSystemMessage ( `⚠️ Some wives failed: ${ result . failed_wives . map ( w => w . wife ) . join ( ', ' ) } ` , true ) ;
1105+ }
10571106 } catch ( err ) {
10581107 wivesGrid . innerHTML = `<div class="sys-msg">Error: ${ escapeHtml ( err . message ) } </div>` ;
10591108 } finally {
10601109 groupSend . disabled = false ;
1061- groupSend . textContent = 'SEND' ;
1110+ groupSend . innerHTML = 'SEND' ;
10621111 }
10631112 }
10641113 await sendGroupMessage ( ) ;
@@ -1123,7 +1172,7 @@ <h4>📈 Market Atlas</h4>
11231172 <div class="modal">
11241173 <div class="modal-header"><h3>Settings</h3><button id="closeSettings">✕</button></div>
11251174 <div class="modal-body">
1126- <div class="settings-section"><h4>Profile</h4><div class="setting-item"><span>Session ID</span><span style="font-family:monospace">${ sessionId . slice ( - 12 ) } </span></div><div class="setting-item"><button id="clearStorageBtn">Clear Storage</button></div></div>
1175+ <div class="settings-section"><h4>Profile</h4><div class="setting-item"><span>Session ID</span><span style="font-family:monospace">${ sessionId . slice ( - 12 ) } </span></div><div class="setting-item"><button id="resetSessionBtn">Reset Session</button></div><div class="setting-item"><button id=" clearStorageBtn">Clear Storage</button></div><div class="setting-item"><button id="clearChatBtn">Clear Chat </button></div></div>
11271176 <div class="settings-section"><h4>Capabilities</h4><div class="setting-item"><label>ADA Loop</label><input type="checkbox" id="settingAdaLoop" ${ adaLoopEnabled ? 'checked' : '' } ></div><div class="setting-item"><label>Structured Mode</label><input type="checkbox" id="settingStructured" ${ structuredMode ? 'checked' : '' } ></div><div class="setting-item"><label>FCL Mode</label><select id="settingFCL"><option value="GREEN" ${ fclMode === 'GREEN' ? 'selected' : '' } >🟢 GREEN (Normal)</option><option value="BLACK" ${ fclMode === 'BLACK' ? 'selected' : '' } >🔴 BLACK (FCL Testing)</option></select></div></div>
11281177 <div class="settings-section"><h4>Polymath Roles</h4><div id="roleGrid" class="role-grid"></div></div>
11291178 <div class="settings-section"><h4>System Health</h4><div id="analyticsContainer" class="analytics-stats"><div class="stat-card">Loading analytics...</div></div></div>
@@ -1149,7 +1198,6 @@ <h4>📈 Market Atlas</h4>
11491198 const memory = data . memory || { } ;
11501199 const integrity = data . integrity || { } ;
11511200 const instruments = data . instruments || { } ;
1152- const growth = data . growth || { } ;
11531201 const wifeRouting = instruments . wife_routing || { } ;
11541202 const roleActivity = instruments . polymath_role_activity || { } ;
11551203 const fclPassRate = instruments . fcl_pass_rate || 0 ;
@@ -1183,7 +1231,23 @@ <h4>📈 Market Atlas</h4>
11831231 }
11841232
11851233 modal . querySelector ( '#closeSettings' ) . onclick = ( ) => modal . remove ( ) ;
1234+ modal . querySelector ( '#resetSessionBtn' ) . onclick = ( ) => {
1235+ sessionId = 'sess_' + Date . now ( ) + '_' + Math . random ( ) . toString ( 36 ) . substr ( 2 , 6 ) ;
1236+ localStorage . setItem ( 'sessionId' , sessionId ) ;
1237+ document . getElementById ( 'sessionDisplay' ) . textContent = `SID: ${ sessionId . slice ( - 8 ) } ` ;
1238+ showToast ( 'Session reset. Reloading...' ) ;
1239+ setTimeout ( ( ) => location . reload ( ) , 1000 ) ;
1240+ } ;
11861241 modal . querySelector ( '#clearStorageBtn' ) . onclick = ( ) => { localStorage . clear ( ) ; location . reload ( ) ; } ;
1242+ modal . querySelector ( '#clearChatBtn' ) . onclick = ( ) => {
1243+ messagesDiv . innerHTML = '' ;
1244+ const welcome = document . createElement ( 'div' ) ; welcome . className = 'msg assistant' ; welcome . setAttribute ( 'data-wife' , 'AION' ) ;
1245+ welcome . innerHTML = `<div class="msg-meta">✨ AION</div><div class="msg-body">Chat cleared. How may I assist you?</div>` ;
1246+ messagesDiv . appendChild ( welcome ) ;
1247+ firstUserMessageSent = false ;
1248+ showToast ( 'Chat cleared.' ) ;
1249+ modal . remove ( ) ;
1250+ } ;
11871251 modal . querySelector ( '#settingAdaLoop' ) . onchange = ( e ) => {
11881252 adaLoopEnabled = e . target . checked ;
11891253 localStorage . setItem ( 'adaLoopEnabled' , adaLoopEnabled ) ;
@@ -1211,7 +1275,7 @@ <h4>📈 Market Atlas</h4>
12111275 }
12121276
12131277 // Event listeners
1214- sendBtn . onclick = sendMessage ;
1278+ sendBtn . onclick = ( ) => sendMessage ( ) ;
12151279 input . addEventListener ( 'keydown' , ( e ) => { if ( e . key === 'Enter' && ! e . shiftKey ) { e . preventDefault ( ) ; sendMessage ( ) ; } } ) ;
12161280 input . addEventListener ( 'input' , ( ) => { charCountSpan . textContent = input . value . length ; input . style . height = 'auto' ; input . style . height = Math . min ( input . scrollHeight , 400 ) + 'px' ; } ) ;
12171281 plusBtn . onclick = ( e ) => { e . stopPropagation ( ) ; actionDropdown . style . display = actionDropdown . style . display === 'none' ? 'flex' : 'none' ; } ;
0 commit comments