PageSourceSearch

https://roycekimmons.com/js/etb/ai-chat.js

js roycekimmons.com collected 2026-09-25 21:04:30 UTC 4,997 bytes, 128 lines download raw bytes

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.