1let aiChatReady = false; 2let aiChatGreeting = null; 3 4// Context windowing configuration 5const AI_CONTEXT_WINDOW_SIZE = 6; // Keep last 6 messages (3 exchanges) 6let aiChatHistory = []; // Store [{role: 'user'|'assistant', content: string}] 7 8async function InitAIChat() { 9 if (aiChatReady) return; 10 aiChatReady = true; 11 12 const html = await GetTemplate('panel-ai-chat'); 13 $(document.body).append(html); 14 15 $('#ai-chat-close').on('click', CloseAIChat); 16 $('#ai-chat-send').on('click', SendAIMessage); 17 $('#ai-chat-input').on('keydown', function (e) { 18 if (e.key === 'Enter' && !e.shiftKey) { 19 e.preventDefault(); 20 SendAIMessage(); 21 } 22 }); 23 24 if (aiChatGreeting) { 25 AppendAIChatMessage('assistant', aiChatGreeting); 26 aiChatGreeting = null; 27 } 28 // Initialize empty history for new chat 29 aiChatHistory = []; 30} 31 32function ToggleAIChat() { 33 InitAIChat().then(() => { 34 const panel = $('#ai-chat-panel'); 35 if (panel.hasClass('d-none')) { 36 panel.removeClass('d-none').css('display', 'flex'); 37 $('#ai-chat-input').focus(); 38 } else { 39 CloseAIChat(); 40 } 41 }); 42} 43 44function CloseAIChat() { 45 $('#ai-chat-panel').addClass('d-none').css('display', ''); 46} 47 48function AppendAIChatMessage(role, text) { 49 const isUser = role === 'user'; 50 const bubble = $('<div></div>') 51 .addClass('d-flex') 52 .addClass(isUser ? 'justify-content-end' : 'justify-content-start'); 53 const inner = $('<div></div>') 54 .addClass('rounded px-3 py-2 small') 55 .css('max-width', '85%') 56 .css('white-space', 'pre-wrap') 57 .css('word-break', 'break-word') 58 .addClass(isUser ? 'btn-edit text-light' : 'bg-secondary-subtle text-body') 59 .text(text); 60 bubble.append(inner); 61 $('#ai-chat-messages').append(bubble); 62 // Scroll to bottom 63 const msgs = document.getElementById('ai-chat-messages'); 64 if (msgs) msgs.scrollTop = msgs.scrollHeight; 65 return inner; 66} 67 68function ResetAIChat() { 69 $('#ai-chat-messages').empty(); 70 $('#ai-chat-input').val(''); 71 aiChatHistory = []; // Clear conversation history 72 const title = $('#ask-ai-fab').data('chapter-title'); 73 const greeting = `Hi there! What would you like to know about "${title || 'this chapter'}"?`; 74 if (aiChatReady) { 75 AppendAIChatMessage('assistant', greeting); 76 } else { 77 aiChatGreeting = greeting; 78 } 79} 80 81async function SendAIMessage() { 82 const input = $('#ai-chat-input'); 83 const question = input.val().trim(); 84 if (!question) return; 85 input.val('').attr('disabled', true); 86 $('#ai-chat-send').attr('disabled', true); 87 88 AppendAIChatMessage('user', question); 89 // Add user message to history 90 aiChatHistory.push({ role: 'user', content: question }); 91 92 const chapterId = $('#ask-ai-fab').data('chapter-id'); 93 const thinking = $('<div></div>').addClass('d-flex justify-content-start');
94 const thinkingInner = $('<div></div>') 95 .addClass('rounded px-3 py-2 bg-secondary-subtle d-flex align-items-center gap-1'); 96 for (let i = 0; i < 3; i++) { 97 $('<span></span>').addClass('spinner-grow spinner-grow-sm text-secondary').attr('role', 'status').css('animation-delay', `${i * 0.15}s`).appendTo(thinkingInner); 98 } 99 thinking.append(thinkingInner); 100 $('#ai-chat-messages').append(thinking); 101 const msgs = document.getElementById('ai-chat-messages'); 102 if (msgs) msgs.scrollTop = msgs.scrollHeight; 103 104 try { 105 // Send current question + recent conversation context (context window) 106 const conversationContext = aiChatHistory.slice(-AI_CONTEXT_WINDOW_SIZE); 107 const json = await API('/ai/ask', { question, chapter_id: chapterId, conversation_history: conversationContext }); 108 let responseBubble; 109 thinking.replaceWith((() => { 110 responseBubble = $('<div></div>').addClass('d-flex justify-content-start'); 111 $('<div></div>').addClass('rounded px-3 py-2 small bg-secondary-subtle text-body').css({ 'max-width': '85%', 'white-space': 'pre-wrap', 'word-break': 'break-word' }).text(json.answer).appendTo(responseBubble); 112 return responseBubble; 113 })()); 114 // Add assistant response to history 115 aiChatHistory.push({ role: 'assistant', content: json.answer }); 116 if (msgs && responseBubble) msgs.scrollTop = responseBubble[0].offsetTop - 80; 117 } catch (error) { 118 thinking.replaceWith((() => { 119 const b = $('<div></div>').addClass('d-flex justify-content-start'); 120 $('<div></div>').addClass('rounded px-3 py-2 small bg-secondary-subtle text-danger').css({ 'max-width': '85%', 'white-space': 'pre-wrap', 'word-break': 'break-word' }).text('Sorry, I was unable to get a response.').appendTo(b); 121 return b; 122 })()); 123 } finally { 124 input.removeAttr('disabled'); 125 $('#ai-chat-send').removeAttr('disabled'); 126 input.focus(); 127 } 128}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.