1+ /**
2+ * HackTricks AI Chat Widget v1.14 – animated typing indicator
3+ * ------------------------------------------------------------------------
4+ * • Replaces the static “…” placeholder with a three‑dot **bouncing** loader
5+ * while waiting for the assistant’s response.
6+ * ------------------------------------------------------------------------
7+ */
8+ ( function ( ) {
9+ const LOG = "[HackTricks-AI]" ;
10+
11+ /* ---------------- User‑tunable constants ---------------- */
12+ const MAX_CONTEXT = 3000 ; // highlighted‑text char limit
13+ const MAX_QUESTION = 500 ; // question char limit
14+ const TOOLTIP_TEXT =
15+ "💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it" ;
16+
17+ const API_BASE = "https://www.hacktricks.ai/api/assistants/threads" ;
18+ const BRAND_RED = "#b31328" ; // HackTricks brand
19+
20+ /* ------------------------------ State ------------------------------ */
21+ let threadId = null ;
22+ let isRunning = false ;
23+
24+ const $ = ( sel , ctx = document ) => ctx . querySelector ( sel ) ;
25+ if ( document . getElementById ( "ht-ai-btn" ) ) { console . warn ( `${ LOG } Widget already injected.` ) ; return ; }
26+ ( document . readyState === "loading" ? document . addEventListener ( "DOMContentLoaded" , init ) : init ( ) ) ;
27+
28+ /* ==================================================================== */
29+ async function init ( ) {
30+ console . log ( `${ LOG } Injecting widget… v1.14` ) ;
31+ await ensureThreadId ( ) ;
32+ injectStyles ( ) ;
33+
34+ const btn = createFloatingButton ( ) ;
35+ createTooltip ( btn ) ;
36+ const panel = createSidebar ( ) ;
37+ const chatLog = $ ( "#ht-ai-chat" ) ;
38+ const sendBtn = $ ( "#ht-ai-send" ) ;
39+ const inputBox = $ ( "#ht-ai-question" ) ;
40+ const resetBtn = $ ( "#ht-ai-reset" ) ;
41+ const closeBtn = $ ( "#ht-ai-close" ) ;
42+
43+ /* ------------------- Selection snapshot ------------------- */
44+ let savedSelection = "" ;
45+ btn . addEventListener ( "pointerdown" , ( ) => { savedSelection = window . getSelection ( ) . toString ( ) . trim ( ) ; } ) ;
46+
47+ /* ------------------- Helpers ------------------------------ */
48+ function addMsg ( text , cls ) {
49+ const b = document . createElement ( "div" ) ;
50+ b . className = `ht-msg ${ cls } ` ;
51+ b . textContent = text ;
52+ chatLog . appendChild ( b ) ;
53+ chatLog . scrollTop = chatLog . scrollHeight ;
54+ return b ;
55+ }
56+ const LOADER_HTML = '<span class="ht-loading"><span></span><span></span><span></span></span>' ;
57+
58+ function setInputDisabled ( d ) { inputBox . disabled = d ; sendBtn . disabled = d ; }
59+ function clearThreadCookie ( ) { document . cookie = "threadId=; Path=/; Max-Age=0" ; threadId = null ; }
60+ function resetConversation ( ) { chatLog . innerHTML = "" ; clearThreadCookie ( ) ; panel . classList . remove ( "open" ) ; }
61+
62+ /* ------------------- Panel open / close ------------------- */
63+ btn . addEventListener ( "click" , ( ) => {
64+ if ( ! savedSelection ) { alert ( "Please highlight some text first to then ask Hacktricks AI about it." ) ; return ; }
65+ if ( savedSelection . length > MAX_CONTEXT ) { alert ( `Highlighted text is too long (${ savedSelection . length } chars). Max allowed: ${ MAX_CONTEXT } .` ) ; return ; }
66+ chatLog . innerHTML = "" ; addMsg ( savedSelection , "ht-context" ) ; panel . classList . add ( "open" ) ; inputBox . focus ( ) ;
67+ } ) ;
68+ closeBtn . addEventListener ( "click" , resetConversation ) ;
69+ resetBtn . addEventListener ( "click" , resetConversation ) ;
70+
71+ /* --------------------------- Messaging --------------------------- */
72+ async function sendMessage ( question , context = null ) {
73+ if ( ! threadId ) await ensureThreadId ( ) ;
74+ if ( isRunning ) { addMsg ( "Please wait until the current operation completes." , "ht-ai" ) ; return ; }
75+
76+ isRunning = true ; setInputDisabled ( true ) ;
77+ const loadingBubble = addMsg ( "" , "ht-ai" ) ;
78+ loadingBubble . innerHTML = LOADER_HTML ;
79+
80+ const content = context ? `Context:\n${ context } \n\nQuestion:\n${ question } ` : question ;
81+ try {
82+ const res = await fetch ( `${ API_BASE } /${ threadId } /messages` , { method :"POST" , credentials :"include" , headers :{ "Content-Type" :"application/json" } , body :JSON . stringify ( { content} ) } ) ;
83+ if ( ! res . ok ) {
84+ let err = `Unknown error: ${ res . status } ` ;
85+ try { const e = await res . json ( ) ; if ( e . error ) err = `Error: ${ e . error } ` ; else if ( res . status === 429 ) err = "Rate limit exceeded. Please try again later." ; } catch ( _ ) { }
86+ loadingBubble . textContent = err ; return ; }
87+ const data = await res . json ( ) ;
88+ loadingBubble . remove ( ) ;
89+ if ( Array . isArray ( data . response ) ) data . response . forEach ( p => { addMsg ( p . type === "text" && p . text && p . text . value ? p . text . value : JSON . stringify ( p ) , "ht-ai" ) ; } ) ;
90+ else if ( typeof data . response === "string" ) addMsg ( data . response , "ht-ai" ) ;
91+ else addMsg ( JSON . stringify ( data , null , 2 ) , "ht-ai" ) ;
92+ } catch ( e ) { console . error ( "Error sending message:" , e ) ; loadingBubble . textContent = "An unexpected error occurred." ; }
93+ finally { isRunning = false ; setInputDisabled ( false ) ; chatLog . scrollTop = chatLog . scrollHeight ; }
94+ }
95+
96+ async function handleSend ( ) { const q = inputBox . value . trim ( ) ; if ( ! q ) return ; if ( q . length > MAX_QUESTION ) { alert ( `Your question is too long (${ q . length } chars). Max allowed: ${ MAX_QUESTION } .` ) ; return ; } inputBox . value = "" ; addMsg ( q , "ht-user" ) ; await sendMessage ( q , savedSelection || null ) ; }
97+ sendBtn . addEventListener ( "click" , handleSend ) ;
98+ inputBox . addEventListener ( "keydown" , e => { if ( e . key === "Enter" && ! e . shiftKey ) { e . preventDefault ( ) ; handleSend ( ) ; } } ) ;
99+ }
100+
101+ /* ==================================================================== */
102+ async function ensureThreadId ( ) { const m = document . cookie . match ( / t h r e a d I d = ( [ ^ ; ] + ) / ) ; if ( m && m [ 1 ] ) { threadId = m [ 1 ] ; return ; } try { const r = await fetch ( API_BASE , { method :"POST" , credentials :"include" } ) ; const d = await r . json ( ) ; if ( ! r . ok || ! d . threadId ) throw new Error ( `${ r . status } ${ r . statusText } ` ) ; threadId = d . threadId ; document . cookie = `threadId=${ threadId } ; Path=/; Secure; SameSite=Strict; Max-Age=7200` ; } catch ( e ) { console . error ( "Error creating threadId:" , e ) ; alert ( "Failed to initialise the conversation. Please refresh and try again." ) ; throw e ; } }
103+
104+ /* ==================================================================== */
105+ function injectStyles ( ) { const css = `
106+ #ht-ai-btn{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);width:60px;height:60px;border-radius:50%;background:#1e1e1e;color:#fff;font-size:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,.4);transition:opacity .2s}
107+ #ht-ai-btn:hover{opacity:.85}
108+ @media(max-width:768px){#ht-ai-btn{display:none}}
109+ #ht-ai-tooltip{position:fixed;padding:6px 8px;background:#111;color:#fff;border-radius:4px;font-size:13px;white-space:pre-wrap;pointer-events:none;opacity:0;transform:translate(-50%,-8px);transition:opacity .15s ease,transform .15s ease;z-index:100000}
110+ #ht-ai-tooltip.show{opacity:1;transform:translate(-50%,-12px)}
111+ #ht-ai-panel{position:fixed;top:0;right:0;height:100%;width:350px;max-width:90vw;background:#000;color:#fff;display:flex;flex-direction:column;transform:translateX(100%);transition:transform .3s ease;z-index:100000;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial,sans-serif}
112+ #ht-ai-panel.open{transform:translateX(0)}
113+ @media(max-width:768px){#ht-ai-panel{display:none}}
114+ #ht-ai-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #333}
115+ #ht-ai-header .ht-actions{display:flex;gap:8px;align-items:center}
116+ #ht-ai-close,#ht-ai-reset{cursor:pointer;font-size:18px;background:none;border:none;color:#fff;padding:0}
117+ #ht-ai-close:hover,#ht-ai-reset:hover{opacity:.7}
118+ #ht-ai-chat{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;font-size:14px}
119+ .ht-msg{max-width:90%;line-height:1.4;padding:10px 12px;border-radius:8px;white-space:pre-wrap;word-wrap:break-word}
120+ .ht-user{align-self:flex-end;background:${ BRAND_RED } }
121+ .ht-ai{align-self:flex-start;background:#222}
122+ .ht-context{align-self:flex-start;background:#444;font-style:italic;font-size:13px}
123+ #ht-ai-input{display:flex;gap:8px;padding:12px 16px;border-top:1px solid #333}
124+ #ht-ai-question{flex:1;min-height:40px;max-height:120px;resize:vertical;padding:8px;border-radius:6px;border:none;font-size:14px}
125+ #ht-ai-send{padding:0 18px;border:none;border-radius:6px;background:${ BRAND_RED } ;color:#fff;font-size:14px;cursor:pointer}
126+ #ht-ai-send:disabled{opacity:.5;cursor:not-allowed}
127+ /* Loader animation */
128+ .ht-loading{display:inline-flex;align-items:center;gap:4px}
129+ .ht-loading span{width:6px;height:6px;border-radius:50%;background:#888;animation:ht-bounce 1.2s infinite ease-in-out}
130+ .ht-loading span:nth-child(2){animation-delay:0.2s}
131+ .ht-loading span:nth-child(3){animation-delay:0.4s}
132+ @keyframes ht-bounce{0%,80%,100%{transform:scale(0);}40%{transform:scale(1);} }
133+ ::selection{background:#ffeb3b;color:#000}
134+ ::-moz-selection{background:#ffeb3b;color:#000}` ;
135+ const s = document . createElement ( "style" ) ; s . id = "ht-ai-style" ; s . textContent = css ; document . head . appendChild ( s ) ; }
136+
137+ function createFloatingButton ( ) { const d = document . createElement ( "div" ) ; d . id = "ht-ai-btn" ; d . textContent = "🤖" ; document . body . appendChild ( d ) ; return d ; }
138+ function createTooltip ( btn ) { const t = document . createElement ( "div" ) ; t . id = "ht-ai-tooltip" ; t . textContent = TOOLTIP_TEXT ; document . body . appendChild ( t ) ; btn . addEventListener ( "mouseenter" , ( ) => { const r = btn . getBoundingClientRect ( ) ; t . style . left = `${ r . left + r . width / 2 } px` ; t . style . top = `${ r . top } px` ; t . classList . add ( "show" ) ; } ) ; btn . addEventListener ( "mouseleave" , ( ) => t . classList . remove ( "show" ) ) ; }
139+ function createSidebar ( ) { const p = document . createElement ( "div" ) ; p . id = "ht-ai-panel" ; p . innerHTML = `<div id="ht-ai-header"><strong>HackTricks Chat</strong><div class="ht-actions"><button id="ht-ai-reset" title="Reset">↺</button><span id="ht-ai-close" title="Close">✖</span></div></div><div id="ht-ai-chat"></div><div id="ht-ai-input"><textarea id="ht-ai-question" placeholder="Type your question…"></textarea><button id="ht-ai-send">Send</button></div>` ; document . body . appendChild ( p ) ; return p ; }
140+ } ) ( ) ;
141+
0 commit comments