Skip to content

Merging to release-5.8: Add "Ask AI" to docs website (#6317) #6407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

buger
Copy link
Member

@buger buger commented May 13, 2025

User description

Add "Ask AI" to docs website (#6317)


PR Type

Enhancement, Documentation


Description

  • Introduces an interactive "Ask AI" chat widget to the docs site.

  • Implements frontend logic for streaming AI responses with markdown/code support.

  • Adds custom CSS for chat UI, markdown, and mobile responsiveness.

  • Integrates the widget into the site layout and header for universal access.


Changes walkthrough 📝

Relevant files
Enhancement
ai-chat-widget.html
Introduce AI chat widget partial with UI and dependencies

tyk-docs/themes/tykio/layouts/partials/ai-chat-widget.html

  • Adds a new partial for the AI chat widget UI.
  • Includes modal overlay, popup, input, and message display.
  • Loads Highlight.js, Marked.js, DOMPurify, and custom CSS.
  • +32/-0   
    chat.js
    Add frontend logic for AI chat widget with streaming         

    tyk-docs/static/js/chat.js

  • Implements frontend logic for chat widget and streaming responses.
  • Handles user input, SSE streaming, markdown rendering, and error
    handling.
  • Integrates syntax highlighting and sanitization for responses.
  • +295/-0 
    chat.css
    Add custom CSS for chat widget and markdown rendering       

    tyk-docs/static/css/chat.css

  • Adds custom styles for chat widget layout and responsiveness.
  • Styles markdown content, typing indicator, and chat popup.
  • Ensures mobile responsiveness for the chat popup.
  • +360/-0 
    baseof.html
    Integrate AI chat widget partial into base layout               

    tyk-docs/themes/tykio/layouts/_default/baseof.html

  • Integrates the AI chat widget partial into the base layout.
  • Ensures widget and its script load on all pages.
  • +3/-0     
    header.html
    Add "Ask AI" button to site header                                             

    tyk-docs/themes/tykio/layouts/partials/header.html

  • Adds "Ask AI" button to the header near the search widget.
  • Ensures easy access to the chat widget from all pages.
  • +5/-0     
    _header.scss
    Style and position "Ask AI" button in header                         

    tyk-docs/assets/scss/_header.scss

  • Styles the "Ask AI" button for visual prominence.
  • Adjusts search widget and button positioning for responsiveness.
  • +28/-0   

    Need help?
  • Type /help how to ... in the comments thread for any questions about PR-Agent usage.
  • Check out the documentation for more information.
  • (cherry picked from commit 6c160f5)
    @buger buger enabled auto-merge (squash) May 13, 2025 07:24
    Copy link
    Contributor

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    🎫 Ticket compliance analysis 🔶

    6317 - Partially compliant

    Compliant requirements:

    • Add an interactive AI chat widget to the docs site.
    • Implement frontend logic for streaming AI responses with markdown and code support.
    • Add custom CSS for chat UI, markdown rendering, and mobile responsiveness.
    • Integrate the widget into the site layout and header for universal access.
    • Ensure the widget is accessible and styled appropriately.
    • Ensure dependencies for markdown rendering and syntax highlighting are loaded.
    • Provide error handling and user feedback for streaming/API failures.

    Non-compliant requirements:

    • Add a preview link (process requirement).

    Requires further human verification:

    • Review AI PR Agent suggestions (process requirement).
    • Add Jira DX PR ticket to the subject (process requirement, for Tyk Members).
    • Add appropriate release labels (process requirement, for Tyk Members).
    • Start changes from latest master (process requirement, for new contributors).
    • Review and follow contribution and technical guidelines (process requirement, for new contributors).
    ⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
    🧪 No relevant tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Error Handling

    The streaming and fetch error handling logic should be validated to ensure users always receive appropriate feedback and that the UI resets correctly after failures or aborts.

    }).catch(error => {
      console.error('Error fetching stream:', error);
      if (typingIndicator) typingIndicator.remove();
    
      // Check if this was an abort error
      if (error.name === 'AbortError') {
        rawContentContainer.textContent = 'Request was cancelled.';
      } else {
        rawContentContainer.textContent = 'Sorry, something went wrong with the streaming connection.';
      }
    
      // Reset UI
      chatStop.classList.add('hidden');
      chatSubmit.classList.remove('hidden');
    });
    Security/Sanitization

    The use of DOMPurify and marked.js for rendering markdown and user input should be carefully reviewed to ensure no XSS or injection vulnerabilities are introduced, especially as both user and AI-generated content are rendered as HTML.

      const sanitizedMessage = DOMPurify.sanitize(message);
      messageElement.innerHTML = `
        <div class="message-bubble user-bubble">
          ${sanitizedMessage}
        </div>
      `;
      chatMessages.appendChild(messageElement);
      chatMessages.scrollTop = chatMessages.scrollHeight;
    
      // Add user message to history
      messagesHistory.push({
        role: "user",
        content: message
      });
    
      // Create and show typing indicator immediately
      const replyElement = document.createElement('div');
      replyElement.className = 'assistant-message';
      replyElement.id = 'current-response'; // Add ID for easy reference
      replyElement.innerHTML = `
        <div class="message-bubble assistant-bubble">
          <div class="raw-content" style="white-space: pre-wrap;"></div>
        </div>
        <div class="typing-indicator">
          <span class="dot"></span>
          <span class="dot"></span>
          <span class="dot"></span>
        </div>
      `;
    
      // Append typing indicator immediately
      chatMessages.appendChild(replyElement);
      chatMessages.scrollTop = chatMessages.scrollHeight;
    
      // Handle streaming response
      handleStreamingResponse(messagesHistory, replyElement);
    }
    
    function handleStreamingResponse(messages, replyElement) {
      // Create a new AbortController for this request
      activeController = new AbortController();
    
      // Get references to elements
      const messageContainer = replyElement.querySelector('.assistant-bubble');
      const rawContentContainer = replyElement.querySelector('.raw-content');
      const typingIndicator = replyElement.querySelector('.typing-indicator');
    
      // Set up headers for SSE
      const headers = new Headers({
        'Accept': 'text/event-stream',
        'Content-Type': 'application/json'
      });
    
      const signal = activeController.signal;
    
      fetch('https://tyk-docs-ask-ai.dokku.tyk.technology/api/stream', {
        method: 'POST',
        headers: headers,
        body: JSON.stringify({ messages: messages }),
        signal: signal
      }).then(response => {
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';
        let accumulatedText = '';
    
        function processStream() {
          reader.read().then(({ done, value }) => {
            if (done) {
              if (typingIndicator) typingIndicator.remove();
              renderMarkdown(accumulatedText, rawContentContainer);
              return;
            }
    
            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n\n');
            buffer = lines.pop() || '';
    
            for (const line of lines) {
              if (line.trim() === '') continue;
    
              const dataMatch = line.match(/^data: (.+)$/m);
              if (!dataMatch) continue;
              const data = dataMatch[1];
    
              if (data === '[DONE]') {
                if (typingIndicator) typingIndicator.remove();
                renderMarkdown(accumulatedText, rawContentContainer);
                return;
              }
    
              try {
                const parsedData = JSON.parse(data);
                if (parsedData.text) {
    
                  if (parsedData.text === "[ERROR]") {
                    accumulatedText += "\n\n**Unexpected failure, please try again**"
                    rawContentContainer.textContent = "\n\n**Unexpected failure, please try again**"
                    chatMessages.scrollTop = chatMessages.scrollHeight;
                  } else {
                    accumulatedText += parsedData.text;
                    rawContentContainer.textContent = accumulatedText;
                    chatMessages.scrollTop = chatMessages.scrollHeight;
                  }
                }
              } catch (error) {
                console.error('Error parsing SSE data:', error);
              }
            }
    
            processStream();
          }).catch(error => {
            console.error('Error reading from stream:', error);
            if (typingIndicator) typingIndicator.remove();
            if (!accumulatedText) {
              rawContentContainer.textContent = 'Sorry, something went wrong with the streaming connection.';
            }
          });
        }
    
        processStream();
      }).catch(error => {
        console.error('Error fetching stream:', error);
        if (typingIndicator) typingIndicator.remove();
    
        // Check if this was an abort error
        if (error.name === 'AbortError') {
          rawContentContainer.textContent = 'Request was cancelled.';
        } else {
          rawContentContainer.textContent = 'Sorry, something went wrong with the streaming connection.';
        }
    
        // Reset UI
        chatStop.classList.add('hidden');
        chatSubmit.classList.remove('hidden');
      });
    }
    
    // Helper to render Markdown at the end
    function renderMarkdown(accumulatedText, container) {
      // Reset UI
      chatStop.classList.add('hidden');
      chatSubmit.classList.remove('hidden');
      activeController = null;
      try {
        const markdownContainer = document.createElement('div');
        markdownContainer.className = 'markdown-content';
    
        // Convert markdown to HTML
        const rawHtml = marked.parse(accumulatedText);
    
        // Sanitize the HTML with DOMPurify before inserting into DOM
        const sanitizedHtml = DOMPurify.sanitize(rawHtml);
        markdownContainer.innerHTML = sanitizedHtml;
    Accessibility & Responsiveness

    The chat widget's accessibility (focus management, keyboard navigation) and mobile responsiveness should be validated in the integrated UI, as these aspects are not fully verifiable from code alone.

    function showPopup() {
      chatPopup.classList.remove('hidden');
      chatOverlay.classList.remove('hidden');
      chatInput.focus();
    }
    
    // Hide popup
    function hidePopup() {
      chatPopup.classList.add('hidden');
      chatOverlay.classList.add('hidden');
    }

    Copy link
    Contributor

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Check API response status before reading

    The code does not check if the fetch response is successful (HTTP 2xx). If the API
    returns an error status, attempting to read the body may cause runtime errors. Add a
    check for response.ok and handle errors appropriately before reading the stream.

    tyk-docs/static/js/chat.js [178-188]

     fetch('https://tyk-docs-ask-ai.dokku.tyk.technology/api/stream', {
       method: 'POST',
       headers: headers,
       body: JSON.stringify({ messages: messages }),
       signal: signal
     }).then(response => {
    +  if (!response.ok) {
    +    throw new Error('API returned an error: ' + response.status);
    +  }
       const reader = response.body.getReader();
       const decoder = new TextDecoder();
       let buffer = '';
       let accumulatedText = '';
       ...
    Suggestion importance[1-10]: 8

    __

    Why: Adding a check for response.ok before reading the response stream is important for robust error handling and prevents runtime errors if the API returns an error status. This is a significant improvement for reliability and correctness.

    Medium
    General
    Prevent empty message submission

    Submitting on 'Enter' should only occur if the input is not empty to prevent sending
    empty messages. Add a check to ensure chatInput.value.trim() is not empty before
    triggering the submit.

    tyk-docs/static/js/chat.js [69-73]

     chatInput.addEventListener('keyup', function (event) {
    -  if (event.key === 'Enter') {
    +  if (event.key === 'Enter' && chatInput.value.trim()) {
         chatSubmit.click();
       }
     });
    Suggestion importance[1-10]: 7

    __

    Why: This suggestion prevents submitting empty messages when pressing Enter, which improves user experience and avoids unnecessary API calls. The change is accurate and relevant, but the impact is moderate as the main functionality would still work without it.

    Medium
    Safely focus input after showing popup

    Calling chatInput.focus() without checking if the element is visible or enabled may
    cause errors if the popup is not fully rendered or the input is disabled. Add a
    check to ensure chatInput is not disabled before focusing.

    tyk-docs/static/js/chat.js [107-108]

     chatPopup.classList.remove('hidden');
     chatOverlay.classList.remove('hidden');
    -chatInput.focus();
    +if (!chatInput.disabled) {
    +  chatInput.focus();
    +}
    Suggestion importance[1-10]: 5

    __

    Why: Checking if chatInput is not disabled before calling focus() is a minor safeguard that prevents potential errors, but such cases are rare in this context. The improvement is correct but has limited practical impact.

    Low

    Copy link

    netlify bot commented May 13, 2025

    PS. Pls add /docs/nightly to the end of url

    Name Link
    🔨 Latest commit a9e5b1b
    🔍 Latest deploy log https://app.netlify.com/sites/tyk-docs/deploys/6822f3c5d2cc510008f9e40f
    😎 Deploy Preview https://deploy-preview-6407--tyk-docs.netlify.app
    📱 Preview on mobile
    Toggle QR Code...

    QR Code

    Use your smartphone camera to open QR code link.

    To edit notification comments on pull requests, go to your Netlify site configuration.

    @buger buger merged commit 989f72a into release-5.8 May 13, 2025
    11 checks passed
    @buger buger deleted the merge/release-5.8/6c160f5353c6dc6383328bd24c793a2c3a22db10 branch May 13, 2025 07:29
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    2 participants