(:Summary: Preferences for [[PmWiki/access keys]] and edit form:) Preferences for [[PmWiki/access keys]] and edit form This page can be used as template for (personal) preferences, or set as default site preference (see below). ->[@ # Access keys - hold Alt (Windows IE), Shift + Alt (Firefox) # or Control (Mac) and tap the indicated key on your keyboard # to trigger the corresponding action. # 'ak_view' => '' , # view page 'ak_edit' => 'e', # edit page 'ak_history' => 'h', # page history # 'ak_print' => '', # print view of page 'ak_recentchanges' => 'c', # Recent Changes 'ak_save' => 's', # save page 'ak_saveedit' => 'u', # save and keep editing 'ak_savedraft' => 'd', # save as draft 'ak_preview' => 'p', # preview page 'ak_textedit' => ',' # focus to edit textarea (at the end) 'ak_em' => 'i', # emphasized text (italic) 'ak_strong' => 'b', # strong text (bold) # 'ak_attach' => '', # attach a file # 'ak_backlinks' => '', # show backlinks # 'ak_logout' => '', # log out # Editing components 'e_rows' => '20', # rows in edit textarea 'e_cols' => '70', # columns in edit textarea 'Site.EditForm' => 'Site.EditForm' # location of EditForm @] To create personal user (browser) preferences, * make a copy of [[{$FullName}?action=source | this page]] somewhere, preferably as @@Profiles.''%green%insert_your_name_here%%''-Preferences@@ * edit that page with your new preferred settings, * select [[{$FullName}?setprefs={$FullName} | Set Preferences of this Page]] on the page containing your newly created settings. -> This sets a preference cookie on your browser which tells PmWiki where to find your personal preference settings. To revert to the PmWiki default preferences * select [[{$Name}?setprefs=| Revert to PmWiki Default Preferences]] to unset the preference cookie See also [[Cookbook:UserConfigurations]] about how to customise the edit form for personal use. Note that by default, parsing of {$FullName} is disabled. To make it the default site preference, add XLPage('prefs', "Site.Preferences"); to a config file (e.g. local/config.php). To disable user preferences entirely set $EnablePrefs = 0; <!-- Global site tag (gtag.js) - Google Analytics --> <script async src="https://www.googletagmanager.com/gtag/js?id=G-14BXFXE5GM"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-14BXFXE5GM'); </script><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Streaming Chatbot</title> <style> /* Collapsed chat container */ #chatbotContainer { position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px; border-radius: 50%; background-color: #007aff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); cursor: pointer; z-index: 1000; animation: pulse 2s infinite; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; } /* Expanded chat container with responsive width and vertical scrolling */ #chatbotContainer.expanded { width: auto; height: auto; min-width: 250px; max-width: 90vw; /* For mobile browsers */ max-height: 90vh; /* Limit height to 90% of the viewport */ padding: 10px; border-radius: 20px; animation: none; background-color: rgba(255, 255, 255, 0.8); /* semi-transparent */ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); overflow-y: auto; /* Vertical scrollbar appears if content exceeds max-height */ overflow-x: auto; right: 20px; left: auto; } /* Double chat window width for desktop browsers */ @media (min-width: 768px) { #chatbotContainer.expanded { max-width: 800px; } } #chatbotIcon { font-size: 30px; color: white; } #chatbotContainer.expanded #chatbotIcon { display: none; } #chatContent { display: none; flex-direction: column; height: auto; min-width: 250px; max-width: 88vw; /* For mobile browsers */ max-height: 88vh; /* Limit height to 90% of the viewport */ padding: 5px; border-radius: 5px; animation: none; background-color: rgba(255, 255, 255, 0.8); /* semi-transparent */ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); overflow-y: auto; /* Vertical scrollbar appears if content exceeds max-height */ overflow-x: hidden; right: 20px; left: auto; } #chatbotContainer.expanded #chatContent { display: flex; } /* Chat box for messages */ #chatBox { flex-grow: 1; overflow-y: auto; border: none; padding: 10px; background-color: #f0f0f0; border-radius: 10px; margin-bottom: 10px; } /* Code block styling: force horizontal scroll if content exceeds width */ .message pre { display: block; max-width: 100%; overflow-x: auto; white-space: pre; -webkit-overflow-scrolling: touch; padding: 10px; background-color: #d3d3d3; color: #030303; border-radius: 5px; margin: 5px 0; } /* Inline code styling */ .message code { background-color: #d3d3d3; color: #030303; padding: 2px 4px; border-radius: 3px; } #messageInput, #sendMessageButton { padding: 10px; border: 1px solid #ccc; border-radius: 20px; font-size: 12px; } #messageInput { flex-grow: 1; margin-right: 10px; border: 1px solid #dcdcdc; } #sendMessageButton { background-color: #007aff; color: white; border: none; cursor: pointer; border-radius: 20px; } #sendMessageButton:hover { background-color: #005bb5; } /* Updated minimize button styling to match send button */ #minimizeButton { background-color: #007aff; color: white; border: none; cursor: pointer; border-radius: 20px; padding: 10px; margin-left: 10px; } #minimizeButton:hover { background-color: #005bb5; } /* Chat message bubble styles */ .message { margin: 5px 0; padding: 10px; border-radius: 20px; max-width: 100%; font-size: 12px; line-height: 1.4; word-wrap: break-word; } .sent { align-self: flex-end; background-color: #007aff; color: white; border-bottom-right-radius: 0; } .received { align-self: flex-start; background-color: #e5e5ea; color: black; border-bottom-left-radius: 0; } .typing { align-self: flex-start; font-style: italic; color: #888; } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } </style> </head> <body> <div id="chatbotContainer"> <div id="chatbotIcon">💬</div> <div id="chatContent"> <div id="chatBox"></div> <!-- Container: input, send button, and minimize button on one row --> <div style="display: flex; align-items: center;"> <input type="text" id="messageInput" placeholder="Ask a question..."> <button id="sendMessageButton" onclick="sendMessage()">Send</button> <button id="minimizeButton" onclick="minimizeChat()">Minimize</button> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script> const chatbotContainer = document.getElementById('chatbotContainer'); const chatContent = document.getElementById('chatContent'); // Toggle the expanded state when clicking the container or icon chatbotContainer.addEventListener('click', function(event) { if (event.target === chatbotContainer || event.target === document.getElementById('chatbotIcon')) { chatbotContainer.classList.toggle('expanded'); } }); function minimizeChat() { chatbotContainer.classList.remove('expanded'); // Restore default max-width if it was changed due to code blocks chatbotContainer.style.maxWidth = ""; } function sendMessage() { const input = document.getElementById('messageInput'); const message = input.value.trim(); if (message) { // Display the sent message displayMessage(message, 'sent'); // Show typing indicator const typingIndicator = displayMessage('AI is typing...', 'typing', true); // Capture page context if needed const pageContent = document.body.innerText.trim(); // Prepare payload const payload = `Context: ${pageContent} \n\n Question: ${message}`; // Open a WebSocket connection const websocket = new WebSocket('wss://hedengren.net/apmchat'); input.value = ''; let streamingText = ''; let receivedMessageElement = null; websocket.onopen = function() { websocket.send(payload); }; websocket.onmessage = function(event) { // On first chunk: remove typing indicator and create a new message container if (!receivedMessageElement) { removeMessage(typingIndicator); receivedMessageElement = document.createElement('div'); receivedMessageElement.className = 'message received'; document.getElementById('chatBox').appendChild(receivedMessageElement); } // Append incoming chunk streamingText += event.data; receivedMessageElement.innerHTML = marked.parse(streamingText); // If a code block (<pre> element) is detected in the message, // ensure the container is set to the desired max-width. if (receivedMessageElement.querySelector('pre')) { chatbotContainer.style.maxWidth = "90vw"; } // Ensure the chat scrolls to the bottom document.getElementById('chatBox').scrollTop = document.getElementById('chatBox').scrollHeight; }; websocket.onerror = function() { removeMessage(typingIndicator); displayMessage('Error in connection', 'received'); websocket.close(); }; websocket.onclose = function() { // Any final cleanup if needed }; } } // Utility: display a message and (optionally) return its element function displayMessage(text, className, returnElement = false) { const messageDiv = document.createElement('div'); messageDiv.className = 'message ' + className; messageDiv.innerHTML = marked.parse(text); document.getElementById('chatBox').appendChild(messageDiv); document.getElementById('chatBox').scrollTop = document.getElementById('chatBox').scrollHeight; if (returnElement) { return messageDiv; } } function removeMessage(element) { if (element && element.parentNode) { element.parentNode.removeChild(element); } } // Send message on Enter key press document.getElementById('messageInput').addEventListener('keypress', function(e) { if (e.key === 'Enter') { sendMessage(); } }); </script> </body> </html>