From cd7ff6f9c137348003493606b1b453637c624fac Mon Sep 17 00:00:00 2001 From: UncleCode Date: Mon, 14 Apr 2025 23:00:47 +0800 Subject: [PATCH] feat(docs): add AI assistant interface and code copy button Add new AI assistant chat interface with features: - Real-time chat with markdown support - Chat history management - Citation tracking - Selection-to-query functionality Also adds code copy button to documentation code blocks and adjusts layout/styling. Breaking changes: None --- docs/md_v2/ask_ai/ask-ai.css | 444 ++++++++++++++ docs/md_v2/ask_ai/ask-ai.js | 603 ++++++++++++++++++++ docs/md_v2/ask_ai/index.html | 64 +++ docs/md_v2/assets/copy_code.js | 62 ++ docs/md_v2/assets/floating_ask_ai_button.js | 39 ++ docs/md_v2/assets/layout.css | 146 ++++- docs/md_v2/assets/selection_ask_ai.js | 109 ++++ docs/md_v2/assets/styles.css | 6 +- docs/md_v2/core/ask-ai.md | 74 +++ mkdocs.yml | 8 +- 10 files changed, 1549 insertions(+), 6 deletions(-) create mode 100644 docs/md_v2/ask_ai/ask-ai.css create mode 100644 docs/md_v2/ask_ai/ask-ai.js create mode 100644 docs/md_v2/ask_ai/index.html create mode 100644 docs/md_v2/assets/copy_code.js create mode 100644 docs/md_v2/assets/floating_ask_ai_button.js create mode 100644 docs/md_v2/assets/selection_ask_ai.js create mode 100644 docs/md_v2/core/ask-ai.md diff --git a/docs/md_v2/ask_ai/ask-ai.css b/docs/md_v2/ask_ai/ask-ai.css new file mode 100644 index 00000000..c464d43b --- /dev/null +++ b/docs/md_v2/ask_ai/ask-ai.css @@ -0,0 +1,444 @@ +/* ==== File: docs/ask_ai/ask_ai.css ==== */ + +/* --- Basic Reset & Font --- */ +body { + /* Attempt to inherit variables from parent window (iframe context) */ + /* Fallback values if variables are not inherited */ + --fallback-bg: #070708; + --fallback-font: #e8e9ed; + --fallback-secondary: #a3abba; + --fallback-primary: #50ffff; + --fallback-primary-dimmed: #09b5a5; + --fallback-border: #1d1d20; + --fallback-code-bg: #1e1e1e; + --fallback-invert-font: #222225; + --font-stack: dm, Monaco, Courier New, monospace, serif; + + font-family: var(--font-stack, "Courier New", monospace); /* Use theme font stack */ + background-color: var(--background-color, var(--fallback-bg)); + color: var(--font-color, var(--fallback-font)); + margin: 0; + padding: 0; + font-size: 14px; /* Match global font size */ + line-height: 1.5em; /* Match global line height */ + height: 100vh; /* Ensure body takes full height */ + overflow: hidden; /* Prevent body scrollbars, panels handle scroll */ + display: flex; /* Use flex for the main container */ +} + +a { + color: var(--secondary-color, var(--fallback-secondary)); + text-decoration: none; + transition: color 0.2s; +} +a:hover { + color: var(--primary-color, var(--fallback-primary)); +} + +/* --- Main Container Layout --- */ +.ai-assistant-container { + display: flex; + width: 100%; + height: 100%; + background-color: var(--background-color, var(--fallback-bg)); +} + +/* --- Sidebar Styling --- */ +.sidebar { + flex-shrink: 0; /* Prevent sidebars from shrinking */ + height: 100%; + display: flex; + flex-direction: column; + /* background-color: var(--code-bg-color, var(--fallback-code-bg)); */ + overflow-y: hidden; /* Header fixed, list scrolls */ +} + +.left-sidebar { + flex-basis: 240px; /* Width of history panel */ + border-right: 1px solid var(--progress-bar-background, var(--fallback-border)); +} + +.right-sidebar { + flex-basis: 280px; /* Width of citations panel */ + border-left: 1px solid var(--progress-bar-background, var(--fallback-border)); +} + +.sidebar header { + padding: 0.6em 1em; + border-bottom: 1px solid var(--progress-bar-background, var(--fallback-border)); + flex-shrink: 0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.sidebar header h3 { + margin: 0; + font-size: 1.1em; + color: var(--font-color, var(--fallback-font)); +} + +.sidebar ul { + list-style: none; + padding: 0; + margin: 0; + overflow-y: auto; /* Enable scrolling for the list */ + flex-grow: 1; /* Allow list to take remaining space */ + padding: 0.5em 0; +} + +.sidebar ul li { + padding: 0.3em 1em; +} +.sidebar ul li.no-citations, +.sidebar ul li.no-history { + color: var(--secondary-color, var(--fallback-secondary)); + font-style: italic; + font-size: 0.9em; + padding-left: 1em; +} + +.sidebar ul li a { + color: var(--secondary-color, var(--fallback-secondary)); + text-decoration: none; + display: block; + padding: 0.2em 0.5em; + border-radius: 3px; + transition: background-color 0.2s, color 0.2s; +} + +.sidebar ul li a:hover { + color: var(--primary-color, var(--fallback-primary)); + background-color: rgba(80, 255, 255, 0.08); /* Use primary color with alpha */ +} +/* Style for active history item */ +#history-list li.active a { + color: var(--primary-dimmed-color, var(--fallback-primary-dimmed)); + font-weight: bold; + background-color: rgba(80, 255, 255, 0.12); +} + +/* --- Chat Panel Styling --- */ +#chat-panel { + flex-grow: 1; /* Take remaining space */ + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; /* Prevent overflow, internal elements handle scroll */ +} + +#chat-messages { + flex-grow: 1; + overflow-y: auto; /* Scrollable chat history */ + padding: 1em 1.5em; + border-bottom: 1px solid var(--progress-bar-background, var(--fallback-border)); +} + +.message { + margin-bottom: 1em; + padding: 0.8em 1.2em; + border-radius: 8px; + max-width: 90%; /* Slightly wider */ + line-height: 1.6; + /* Apply pre-wrap for better handling of spaces/newlines AND wrapping */ + white-space: pre-wrap; + word-wrap: break-word; /* Ensure long words break */ +} + +.user-message { + background-color: var(--progress-bar-background, var(--fallback-border)); /* User message background */ + color: var(--font-color, var(--fallback-font)); + margin-left: auto; /* Align user messages to the right */ + text-align: left; +} + +.ai-message { + background-color: var(--code-bg-color, var(--fallback-code-bg)); /* AI message background */ + color: var(--font-color, var(--fallback-font)); + margin-right: auto; /* Align AI messages to the left */ + border: 1px solid var(--progress-bar-background, var(--fallback-border)); +} +.ai-message.welcome-message { + border: none; + background-color: transparent; + max-width: 100%; + text-align: center; + color: var(--secondary-color, var(--fallback-secondary)); + white-space: normal; +} + +/* Styles for code within messages */ +.ai-message code { + background-color: var(--invert-font-color, var(--fallback-invert-font)) !important; /* Use light bg for code */ + /* color: var(--background-color, var(--fallback-bg)) !important; Dark text */ + padding: 0.1em 0.4em; + border-radius: 4px; + font-size: 0.9em; +} +.ai-message pre { + background-color: var(--invert-font-color, var(--fallback-invert-font)) !important; + color: var(--background-color, var(--fallback-bg)) !important; + padding: 1em; + border-radius: 5px; + overflow-x: auto; + margin: 0.8em 0; + white-space: pre; +} +.ai-message pre code { + background-color: transparent !important; + padding: 0; + font-size: inherit; +} + +/* Override white-space for specific elements generated by Markdown */ +.ai-message p, +.ai-message ul, +.ai-message ol, +.ai-message blockquote { + white-space: normal; /* Allow standard wrapping for block elements */ +} + +/* --- Markdown Element Styling within Messages --- */ +.message p { + margin-top: 0; + margin-bottom: 0.5em; +} +.message p:last-child { + margin-bottom: 0; +} +.message ul, +.message ol { + margin: 0.5em 0 0.5em 1.5em; + padding: 0; +} +.message li { + margin-bottom: 0.2em; +} + +/* Code block styling (adjusts previous rules slightly) */ +.message code { + /* Inline code */ + background-color: var(--invert-font-color, var(--fallback-invert-font)) !important; + color: var(--font-color); + padding: 0.1em 0.4em; + border-radius: 4px; + font-size: 0.9em; + /* Ensure inline code breaks nicely */ + word-break: break-all; + white-space: normal; /* Allow inline code to wrap if needed */ +} +.message pre { + /* Code block container */ + background-color: var(--invert-font-color, var(--fallback-invert-font)) !important; + color: var(--background-color, var(--fallback-bg)) !important; + padding: 1em; + border-radius: 5px; + overflow-x: auto; + margin: 0.8em 0; + font-size: 0.9em; /* Slightly smaller code blocks */ +} +.message pre code { + /* Code within code block */ + background-color: transparent !important; + padding: 0; + font-size: inherit; + word-break: normal; /* Don't break words in code blocks */ + white-space: pre; /* Preserve whitespace strictly in code blocks */ +} + +/* Thinking indicator */ +.message-thinking { + display: inline-block; + width: 5px; + height: 5px; + background-color: var(--primary-color, var(--fallback-primary)); + border-radius: 50%; + margin-left: 8px; + vertical-align: middle; + animation: thinking 1s infinite ease-in-out; +} +@keyframes thinking { + 0%, + 100% { + opacity: 0.5; + transform: scale(0.8); + } + 50% { + opacity: 1; + transform: scale(1.2); + } +} + +/* --- Thinking Indicator (Blinking Cursor Style) --- */ +.thinking-indicator-cursor { + display: inline-block; + width: 10px; /* Width of the cursor */ + height: 1.1em; /* Match line height */ + background-color: var(--primary-color, var(--fallback-primary)); + margin-left: 5px; + vertical-align: text-bottom; /* Align with text baseline */ + animation: blink-cursor 1s step-end infinite; +} + +@keyframes blink-cursor { + from, + to { + background-color: transparent; + } + 50% { + background-color: var(--primary-color, var(--fallback-primary)); + } +} + +#chat-input-area { + flex-shrink: 0; /* Prevent input area from shrinking */ + padding: 1em 1.5em; + display: flex; + align-items: flex-end; /* Align items to bottom */ + gap: 10px; + background-color: var(--code-bg-color, var(--fallback-code-bg)); /* Match sidebars */ +} + +#chat-input-area textarea { + flex-grow: 1; + padding: 0.8em 1em; + border: 1px solid var(--progress-bar-background, var(--fallback-border)); + background-color: var(--background-color, var(--fallback-bg)); + color: var(--font-color, var(--fallback-font)); + border-radius: 5px; + resize: none; /* Disable manual resize */ + font-family: inherit; + font-size: 1em; + line-height: 1.4; + max-height: 150px; /* Limit excessive height */ + overflow-y: auto; + /* rows: 2; */ +} + +#chat-input-area button { + /* Basic button styling - maybe inherit from main theme? */ + padding: 0.6em 1.2em; + border: 1px solid var(--primary-dimmed-color, var(--fallback-primary-dimmed)); + background-color: var(--primary-dimmed-color, var(--fallback-primary-dimmed)); + color: var(--background-color, var(--fallback-bg)); + border-radius: 5px; + cursor: pointer; + font-size: 0.9em; + transition: background-color 0.2s, border-color 0.2s; + height: min-content; /* Align with bottom of textarea */ +} + +#chat-input-area button:hover { + background-color: var(--primary-color, var(--fallback-primary)); + border-color: var(--primary-color, var(--fallback-primary)); +} +#chat-input-area button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.loading-indicator { + font-size: 0.9em; + color: var(--secondary-color, var(--fallback-secondary)); + margin-right: 10px; + align-self: center; +} + +/* --- Buttons --- */ +/* Inherit some button styles if possible */ +.btn.btn-sm { + color: var(--font-color, var(--fallback-font)); + padding: 0.2em 0.5em; + font-size: 0.8em; + border: 1px solid var(--secondary-color, var(--fallback-secondary)); + background: none; + border-radius: 3px; + cursor: pointer; +} +.btn.btn-sm:hover { + border-color: var(--font-color, var(--fallback-font)); + background-color: var(--progress-bar-background, var(--fallback-border)); +} + +/* --- Basic Responsiveness --- */ +@media screen and (max-width: 900px) { + .left-sidebar { + flex-basis: 200px; /* Shrink history */ + } + .right-sidebar { + flex-basis: 240px; /* Shrink citations */ + } +} + +@media screen and (max-width: 768px) { + /* Stack layout on mobile? Or hide sidebars? Hiding for now */ + .sidebar { + display: none; /* Hide sidebars on small screens */ + } + /* Could add toggle buttons later */ +} + + +/* ==== File: docs/ask_ai/ask-ai.css (Updates V4 - Delete Button) ==== */ + + +.sidebar ul li { + /* Use flexbox to align link and delete button */ + display: flex; + justify-content: space-between; + align-items: center; + padding: 0; /* Remove padding from li, add to link/button */ + margin: 0.1em 0; /* Small vertical margin */ +} + +.sidebar ul li a { + /* Link takes most space */ + flex-grow: 1; + padding: 0.3em 0.5em 0.3em 1em; /* Adjust padding */ + /* Make ellipsis work for long titles */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + /* Keep existing link styles */ + color: var(--secondary-color, var(--fallback-secondary)); + text-decoration: none; + display: block; + border-radius: 3px; + transition: background-color 0.2s, color 0.2s; +} +.sidebar ul li a:hover { + color: var(--primary-color, var(--fallback-primary)); + background-color: rgba(80, 255, 255, 0.08); +} + +/* Style for active history item's link */ +#history-list li.active a { + color: var(--primary-dimmed-color, var(--fallback-primary-dimmed)); + font-weight: bold; + background-color: rgba(80, 255, 255, 0.12); +} + +/* --- Delete Chat Button --- */ +.delete-chat-btn { + flex-shrink: 0; /* Don't shrink */ + background: none; + border: none; + color: var(--secondary-color, var(--fallback-secondary)); + cursor: pointer; + padding: 0.4em 0.8em; /* Padding around icon */ + font-size: 0.9em; + opacity: 0.5; /* Dimmed by default */ + transition: opacity 0.2s, color 0.2s; + margin-left: 5px; /* Space between link and button */ + border-radius: 3px; +} + +.sidebar ul li:hover .delete-chat-btn, +.delete-chat-btn:hover { + opacity: 1; /* Show fully on hover */ + color: var(--error-color, #ff3c74); /* Use error color on hover */ +} +.delete-chat-btn:focus { + outline: 1px dashed var(--error-color, #ff3c74); /* Accessibility */ + opacity: 1; +} diff --git a/docs/md_v2/ask_ai/ask-ai.js b/docs/md_v2/ask_ai/ask-ai.js new file mode 100644 index 00000000..2710923e --- /dev/null +++ b/docs/md_v2/ask_ai/ask-ai.js @@ -0,0 +1,603 @@ +// ==== File: docs/ask_ai/ask-ai.js (Marked, Streaming, History) ==== + +document.addEventListener("DOMContentLoaded", () => { + console.log("AI Assistant JS V2 Loaded"); + + // --- DOM Element Selectors --- + const historyList = document.getElementById("history-list"); + const newChatButton = document.getElementById("new-chat-button"); + const chatMessages = document.getElementById("chat-messages"); + const chatInput = document.getElementById("chat-input"); + const sendButton = document.getElementById("send-button"); + const citationsList = document.getElementById("citations-list"); + + // --- Constants --- + const CHAT_INDEX_KEY = "aiAssistantChatIndex_v1"; + const CHAT_PREFIX = "aiAssistantChat_v1_"; + + // --- State --- + let currentChatId = null; + let conversationHistory = []; // Holds message objects { sender: 'user'/'ai', text: '...' } + let isThinking = false; + let streamInterval = null; // To control the streaming interval + + // --- Event Listeners --- + sendButton.addEventListener("click", handleSendMessage); + chatInput.addEventListener("keydown", handleInputKeydown); + newChatButton.addEventListener("click", handleNewChat); + chatInput.addEventListener("input", autoGrowTextarea); + + // --- Initialization --- + loadChatHistoryIndex(); // Load history list on startup + const initialQuery = checkForInitialQuery(window.parent.location); // Check for query param + if (!initialQuery) { + loadInitialChat(); // Load normally if no query + } + + // --- Core Functions --- + + function handleSendMessage() { + const userMessageText = chatInput.value.trim(); + if (!userMessageText || isThinking) return; + + setThinking(true); // Start thinking state + + // Add user message to state and UI + const userMessage = { sender: "user", text: userMessageText }; + conversationHistory.push(userMessage); + addMessageToChat(userMessage, false); // Add user message without parsing markdown + + chatInput.value = ""; + autoGrowTextarea(); // Reset textarea height + + // Prepare for AI response (create empty div) + const aiMessageDiv = addMessageToChat({ sender: "ai", text: "" }, true); // Add empty div with thinking indicator + + // TODO: Generate fingerprint/JWT here + + // TODO: Send `conversationHistory` + JWT to backend API + // Replace placeholder below with actual API call + // The backend should ideally return a stream of text tokens + + // --- Placeholder Streaming Simulation --- + const simulatedFullResponse = `Okay, Here’s a minimal Python script that creates an AsyncWebCrawler, fetches a webpage, and prints the first 300 characters of its Markdown output: + +\`\`\`python +import asyncio +from crawl4ai import AsyncWebCrawler + +async def main(): + async with AsyncWebCrawler() as crawler: + result = await crawler.arun("https://example.com") + print(result.markdown[:300]) # Print first 300 chars + +if __name__ == "__main__": + asyncio.run(main()) +\`\`\` + +A code snippet: \`crawler.run()\`. Check the [quickstart](/core/quickstart).`; + + // Simulate receiving the response stream + streamSimulatedResponse(aiMessageDiv, simulatedFullResponse); + + // // Simulate receiving citations *after* stream starts (or with first chunk) + // setTimeout(() => { + // addCitations([ + // { title: "Simulated Doc 1", url: "#sim1" }, + // { title: "Another Concept", url: "#sim2" }, + // ]); + // }, 500); // Citations appear shortly after thinking starts + } + + function handleInputKeydown(event) { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + handleSendMessage(); + } + } + + function addMessageToChat(message, addThinkingIndicator = false) { + const messageDiv = document.createElement("div"); + messageDiv.classList.add("message", `${message.sender}-message`); + + // Parse markdown and set HTML + messageDiv.innerHTML = message.text ? marked.parse(message.text) : ""; + + if (message.sender === "ai") { + // Apply Syntax Highlighting AFTER setting innerHTML + messageDiv.querySelectorAll("pre code:not(.hljs)").forEach((block) => { + if (typeof hljs !== "undefined") { + // Check if already highlighted to prevent double-highlighting issues + if (!block.classList.contains("hljs")) { + hljs.highlightElement(block); + } + } else { + console.warn("highlight.js (hljs) not found for syntax highlighting."); + } + }); + + // Add thinking indicator if needed (and not already present) + if (addThinkingIndicator && !message.text && !messageDiv.querySelector(".thinking-indicator-cursor")) { + const thinkingDiv = document.createElement("div"); + thinkingDiv.className = "thinking-indicator-cursor"; + messageDiv.appendChild(thinkingDiv); + } + } else { + // User messages remain plain text + // messageDiv.textContent = message.text; + } + + // wrap each pre in a div.terminal + messageDiv.querySelectorAll("pre").forEach((block) => { + const wrapper = document.createElement("div"); + wrapper.className = "terminal"; + block.parentNode.insertBefore(wrapper, block); + wrapper.appendChild(block); + }); + + chatMessages.appendChild(messageDiv); + // Scroll only if user is near the bottom? (More advanced) + // Simple scroll for now: + scrollToBottom(); + return messageDiv; // Return the created element + } + + function streamSimulatedResponse(messageDiv, fullText) { + const thinkingIndicator = messageDiv.querySelector(".thinking-indicator-cursor"); + if (thinkingIndicator) thinkingIndicator.remove(); + + const tokens = fullText.split(/(\s+)/); + let currentText = ""; + let tokenIndex = 0; + // Clear previous interval just in case + if (streamInterval) clearInterval(streamInterval); + + streamInterval = setInterval(() => { + const cursorSpan = ''; // Cursor for streaming + if (tokenIndex < tokens.length) { + currentText += tokens[tokenIndex]; + // Render intermediate markdown + cursor + messageDiv.innerHTML = marked.parse(currentText + cursorSpan); + // Re-highlight code blocks on each stream update - might be slightly inefficient + // but ensures partial code blocks look okay. Highlight only final on completion. + // messageDiv.querySelectorAll('pre code:not(.hljs)').forEach((block) => { + // hljs.highlightElement(block); + // }); + scrollToBottom(); // Keep scrolling as content streams + tokenIndex++; + } else { + // Streaming finished + clearInterval(streamInterval); + streamInterval = null; + + // Final render without cursor + messageDiv.innerHTML = marked.parse(currentText); + + // === Final Syntax Highlighting === + messageDiv.querySelectorAll("pre code:not(.hljs)").forEach((block) => { + if (typeof hljs !== "undefined" && !block.classList.contains("hljs")) { + hljs.highlightElement(block); + } + }); + + // === Extract Citations === + const citations = extractMarkdownLinks(currentText); + + // Wrap each pre in a div.terminal + messageDiv.querySelectorAll("pre").forEach((block) => { + const wrapper = document.createElement("div"); + wrapper.className = "terminal"; + block.parentNode.insertBefore(wrapper, block); + wrapper.appendChild(block); + }); + + const aiMessage = { sender: "ai", text: currentText, citations: citations }; + conversationHistory.push(aiMessage); + updateCitationsDisplay(); + saveCurrentChat(); + setThinking(false); + } + }, 50); // Adjust speed + } + + // === NEW Function to Extract Links === + function extractMarkdownLinks(markdownText) { + const regex = /\[([^\]]+)\]\(([^)]+)\)/g; // [text](url) + const citations = []; + let match; + while ((match = regex.exec(markdownText)) !== null) { + // Avoid adding self-links from within the citations list if AI includes them + if (!match[2].startsWith("#citation-")) { + citations.push({ + title: match[1].trim(), + url: match[2].trim(), + }); + } + } + // Optional: Deduplicate links based on URL + const uniqueCitations = citations.filter( + (citation, index, self) => index === self.findIndex((c) => c.url === citation.url) + ); + return uniqueCitations; + } + + // === REVISED Function to Display Citations === + function updateCitationsDisplay() { + let lastCitations = null; + // Find the most recent AI message with citations + for (let i = conversationHistory.length - 1; i >= 0; i--) { + if ( + conversationHistory[i].sender === "ai" && + conversationHistory[i].citations && + conversationHistory[i].citations.length > 0 + ) { + lastCitations = conversationHistory[i].citations; + break; // Found the latest citations + } + } + + citationsList.innerHTML = ""; // Clear previous + if (!lastCitations) { + citationsList.innerHTML = '
  • No citations available.
  • '; + return; + } + + lastCitations.forEach((citation, index) => { + const li = document.createElement("li"); + const a = document.createElement("a"); + // Generate a unique ID for potential internal linking if needed + // a.id = `citation-${index}`; + a.href = citation.url || "#"; + a.textContent = citation.title; + a.target = "_top"; // Open in main window + li.appendChild(a); + citationsList.appendChild(li); + }); + } + + function addCitations(citations) { + citationsList.innerHTML = ""; // Clear + if (!citations || citations.length === 0) { + citationsList.innerHTML = '
  • No citations available.
  • '; + return; + } + citations.forEach((citation) => { + const li = document.createElement("li"); + const a = document.createElement("a"); + a.href = citation.url || "#"; + a.textContent = citation.title; + a.target = "_top"; // Open in main window + li.appendChild(a); + citationsList.appendChild(li); + }); + } + + function setThinking(thinking) { + isThinking = thinking; + sendButton.disabled = thinking; + chatInput.disabled = thinking; + chatInput.placeholder = thinking ? "AI is responding..." : "Ask about Crawl4AI..."; + // Stop any existing stream if we start thinking again (e.g., rapid resend) + if (thinking && streamInterval) { + clearInterval(streamInterval); + streamInterval = null; + } + } + + function autoGrowTextarea() { + chatInput.style.height = "auto"; + chatInput.style.height = `${chatInput.scrollHeight}px`; + } + + function scrollToBottom() { + chatMessages.scrollTop = chatMessages.scrollHeight; + } + + // --- Query Parameter Handling --- + function checkForInitialQuery(locationToCheck) { + // <-- Receive location object + if (!locationToCheck) { + console.warn("Ask AI: Could not access parent window location."); + return false; + } + const urlParams = new URLSearchParams(locationToCheck.search); // <-- Use passed location's search string + const encodedQuery = urlParams.get("qq"); // <-- Use 'qq' + + if (encodedQuery) { + console.log("Initial query found (qq):", encodedQuery); + try { + const decodedText = decodeURIComponent(escape(atob(encodedQuery))); + console.log("Decoded query:", decodedText); + + // Start new chat immediately + handleNewChat(true); + + // Delay setting input and sending message slightly + setTimeout(() => { + chatInput.value = decodedText; + autoGrowTextarea(); + handleSendMessage(); + + // Clean the PARENT window's URL + try { + const cleanUrl = locationToCheck.pathname; + // Use parent's history object + window.parent.history.replaceState({}, window.parent.document.title, cleanUrl); + } catch (e) { + console.warn("Ask AI: Could not clean parent URL using replaceState.", e); + // This might fail due to cross-origin restrictions if served differently, + // but should work fine with mkdocs serve on the same origin. + } + }, 100); + + return true; // Query processed + } catch (e) { + console.error("Error decoding initial query (qq):", e); + // Clean the PARENT window's URL even on error + try { + const cleanUrl = locationToCheck.pathname; + window.parent.history.replaceState({}, window.parent.document.title, cleanUrl); + } catch (cleanError) { + console.warn("Ask AI: Could not clean parent URL after decode error.", cleanError); + } + return false; + } + } + return false; // No 'qq' query found + } + + // --- History Management --- + + function handleNewChat(isFromQuery = false) { + if (isThinking) return; // Don't allow new chat while responding + + // Only save if NOT triggered immediately by a query parameter load + if (!isFromQuery) { + saveCurrentChat(); + } + + currentChatId = `chat_${Date.now()}`; + conversationHistory = []; // Clear message history state + chatMessages.innerHTML = ""; // Start with clean slate for query + if (!isFromQuery) { + // Show welcome only if manually started + chatMessages.innerHTML = + '
    Started a new chat! Ask me anything about Crawl4AI.
    '; + } + addCitations([]); // Clear citations + updateCitationsDisplay(); // Clear UI + + // Add to index and save + let index = loadChatIndex(); + // Generate a generic title initially, update later + const newTitle = isFromQuery ? "Chat from Selection" : `Chat ${new Date().toLocaleString()}`; + // index.unshift({ id: currentChatId, title: `Chat ${new Date().toLocaleString()}` }); // Add to start + index.unshift({ id: currentChatId, title: newTitle }); + saveChatIndex(index); + + renderHistoryList(index); // Update UI + setActiveHistoryItem(currentChatId); + saveCurrentChat(); // Save the empty new chat state + } + + function loadChat(chatId) { + if (isThinking || chatId === currentChatId) return; + + // Check if chat data actually exists before proceeding + const storedChat = localStorage.getItem(CHAT_PREFIX + chatId); + if (storedChat === null) { + console.warn(`Attempted to load non-existent chat: ${chatId}. Removing from index.`); + deleteChatData(chatId); // Clean up index + loadChatHistoryIndex(); // Reload history list + loadInitialChat(); // Load next available chat + return; + } + + console.log(`Loading chat: ${chatId}`); + saveCurrentChat(); // Save current before switching + + try { + conversationHistory = JSON.parse(storedChat); + currentChatId = chatId; + renderChatMessages(conversationHistory); + updateCitationsDisplay(); + setActiveHistoryItem(chatId); + } catch (e) { + console.error("Error loading chat:", chatId, e); + alert("Failed to load chat data."); + conversationHistory = []; + renderChatMessages(conversationHistory); + updateCitationsDisplay(); + } + } + + function saveCurrentChat() { + if (currentChatId && conversationHistory.length > 0) { + try { + localStorage.setItem(CHAT_PREFIX + currentChatId, JSON.stringify(conversationHistory)); + console.log(`Chat ${currentChatId} saved.`); + + // Update title in index (e.g., use first user message) + let index = loadChatIndex(); + const currentItem = index.find((item) => item.id === currentChatId); + if ( + currentItem && + conversationHistory[0]?.sender === "user" && + !currentItem.title.startsWith("Chat about:") + ) { + currentItem.title = `Chat about: ${conversationHistory[0].text.substring(0, 30)}...`; + saveChatIndex(index); + // Re-render history list if title changed - small optimization needed here maybe + renderHistoryList(index); + setActiveHistoryItem(currentChatId); // Re-set active after re-render + } + } catch (e) { + console.error("Error saving chat:", currentChatId, e); + // Handle potential storage full errors + if (e.name === "QuotaExceededError") { + alert("Local storage is full. Cannot save chat history."); + // Consider implementing history pruning logic here + } + } + } else if (currentChatId) { + // Save empty state for newly created chats if needed, or remove? + localStorage.setItem(CHAT_PREFIX + currentChatId, JSON.stringify([])); + } + } + + function loadChatIndex() { + try { + const storedIndex = localStorage.getItem(CHAT_INDEX_KEY); + return storedIndex ? JSON.parse(storedIndex) : []; + } catch (e) { + console.error("Error loading chat index:", e); + return []; // Return empty array on error + } + } + + function saveChatIndex(indexArray) { + try { + localStorage.setItem(CHAT_INDEX_KEY, JSON.stringify(indexArray)); + } catch (e) { + console.error("Error saving chat index:", e); + } + } + + function renderHistoryList(indexArray) { + historyList.innerHTML = ""; // Clear existing + if (!indexArray || indexArray.length === 0) { + historyList.innerHTML = '
  • No past chats found.
  • '; + return; + } + indexArray.forEach((item) => { + const li = document.createElement("li"); + li.dataset.chatId = item.id; // Add ID to li for easier selection + + const a = document.createElement("a"); + a.href = "#"; + a.dataset.chatId = item.id; + a.textContent = item.title || `Chat ${item.id.split("_")[1] || item.id}`; + a.title = a.textContent; // Tooltip for potentially long titles + a.addEventListener("click", (e) => { + e.preventDefault(); + loadChat(item.id); + }); + + // === Add Delete Button === + const deleteBtn = document.createElement("button"); + deleteBtn.className = "delete-chat-btn"; + deleteBtn.innerHTML = "✕"; // Trash can emoji/icon (or use text/SVG/FontAwesome) + deleteBtn.title = "Delete Chat"; + deleteBtn.dataset.chatId = item.id; // Store ID on button too + deleteBtn.addEventListener("click", handleDeleteChat); + + li.appendChild(a); + li.appendChild(deleteBtn); // Append button to the list item + historyList.appendChild(li); + }); + } + + function renderChatMessages(messages) { + chatMessages.innerHTML = ""; // Clear existing messages + messages.forEach((message) => { + // Ensure highlighting is applied when loading from history + addMessageToChat(message, false); + }); + if (messages.length === 0) { + chatMessages.innerHTML = + '
    Chat history loaded. Ask a question!
    '; + } + // Scroll to bottom after loading messages + scrollToBottom(); + } + + function setActiveHistoryItem(chatId) { + document.querySelectorAll("#history-list li").forEach((li) => li.classList.remove("active")); + // Select the LI element directly now + const activeLi = document.querySelector(`#history-list li[data-chat-id="${chatId}"]`); + if (activeLi) { + activeLi.classList.add("active"); + } + } + + function loadInitialChat() { + const index = loadChatIndex(); + if (index.length > 0) { + loadChat(index[0].id); + } else { + // Check if handleNewChat wasn't already called by query handler + if (!currentChatId) { + handleNewChat(); + } + } + } + + function loadChatHistoryIndex() { + const index = loadChatIndex(); + renderHistoryList(index); + if (currentChatId) setActiveHistoryItem(currentChatId); + } + + // === NEW Function to Handle Delete Click === + function handleDeleteChat(event) { + event.stopPropagation(); // Prevent triggering loadChat on the link behind it + const button = event.currentTarget; + const chatIdToDelete = button.dataset.chatId; + + if (!chatIdToDelete) return; + + // Confirmation dialog + if ( + window.confirm( + `Are you sure you want to delete this chat session?\n"${ + button.previousElementSibling?.textContent || "Chat " + chatIdToDelete + }"` + ) + ) { + console.log(`Deleting chat: ${chatIdToDelete}`); + + // Perform deletion + const updatedIndex = deleteChatData(chatIdToDelete); + + // If the deleted chat was the currently active one, load another chat + if (currentChatId === chatIdToDelete) { + currentChatId = null; // Reset current ID + conversationHistory = []; // Clear state + if (updatedIndex.length > 0) { + // Load the new top chat (most recent remaining) + loadChat(updatedIndex[0].id); + } else { + // No chats left, start a new one + handleNewChat(); + } + } else { + // If a different chat was deleted, just re-render the list + renderHistoryList(updatedIndex); + // Re-apply active state in case IDs shifted (though they shouldn't) + setActiveHistoryItem(currentChatId); + } + } + } + + // === NEW Function to Delete Chat Data === + function deleteChatData(chatId) { + // Remove chat data + localStorage.removeItem(CHAT_PREFIX + chatId); + + // Update index + let index = loadChatIndex(); + index = index.filter((item) => item.id !== chatId); + saveChatIndex(index); + + console.log(`Chat ${chatId} data and index entry removed.`); + return index; // Return the updated index + } + + // --- Virtual Scrolling Placeholder --- + // NOTE: Virtual scrolling is complex. For now, we do direct rendering. + // If performance becomes an issue with very long chats/history, + // investigate libraries like 'simple-virtual-scroll' or 'virtual-scroller'. + // You would replace parts of `renderChatMessages` and `renderHistoryList` + // to work with the chosen library's API (providing data and item renderers). + console.warn("Virtual scrolling not implemented. Performance may degrade with very long chat histories."); +}); diff --git a/docs/md_v2/ask_ai/index.html b/docs/md_v2/ask_ai/index.html new file mode 100644 index 00000000..5fe79b12 --- /dev/null +++ b/docs/md_v2/ask_ai/index.html @@ -0,0 +1,64 @@ + + + + + + Crawl4AI Assistant + + + + + + + + +
    + + + + + +
    +
    + +
    + Welcome to the Crawl4AI Assistant! How can I help you today? +
    +
    +
    + + + + +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/docs/md_v2/assets/copy_code.js b/docs/md_v2/assets/copy_code.js new file mode 100644 index 00000000..20e6be4f --- /dev/null +++ b/docs/md_v2/assets/copy_code.js @@ -0,0 +1,62 @@ +// ==== File: docs/assets/copy_code.js ==== + +document.addEventListener('DOMContentLoaded', () => { + // Target specifically code blocks within the main content area + const codeBlocks = document.querySelectorAll('#terminal-mkdocs-main-content pre > code'); + + codeBlocks.forEach((codeElement) => { + const preElement = codeElement.parentElement; // The
     tag
    +
    +        // Ensure the 
     tag can contain a positioned button
    +        if (window.getComputedStyle(preElement).position === 'static') {
    +            preElement.style.position = 'relative';
    +        }
    +
    +        // Create the button
    +        const copyButton = document.createElement('button');
    +        copyButton.className = 'copy-code-button';
    +        copyButton.type = 'button';
    +        copyButton.setAttribute('aria-label', 'Copy code to clipboard');
    +        copyButton.title = 'Copy code to clipboard';
    +        copyButton.innerHTML = 'Copy'; // Or use an icon like an SVG or FontAwesome class
    +
    +        // Append the button to the 
     element
    +        preElement.appendChild(copyButton);
    +
    +        // Add click event listener
    +        copyButton.addEventListener('click', () => {
    +            copyCodeToClipboard(codeElement, copyButton);
    +        });
    +    });
    +
    +    async function copyCodeToClipboard(codeElement, button) {
    +        // Use innerText to get the rendered text content, preserving line breaks
    +        const textToCopy = codeElement.innerText;
    +
    +        try {
    +            await navigator.clipboard.writeText(textToCopy);
    +
    +            // Visual feedback
    +            button.innerHTML = 'Copied!';
    +            button.classList.add('copied');
    +            button.disabled = true; // Temporarily disable
    +
    +            // Revert button state after a short delay
    +            setTimeout(() => {
    +                button.innerHTML = 'Copy';
    +                button.classList.remove('copied');
    +                button.disabled = false;
    +            }, 2000); // Show "Copied!" for 2 seconds
    +
    +        } catch (err) {
    +            console.error('Failed to copy code: ', err);
    +            // Optional: Provide error feedback on the button
    +            button.innerHTML = 'Error';
    +            setTimeout(() => {
    +                button.innerHTML = 'Copy';
    +            }, 2000);
    +        }
    +    }
    +
    +    console.log("Copy Code Button script loaded.");
    +});
    \ No newline at end of file
    diff --git a/docs/md_v2/assets/floating_ask_ai_button.js b/docs/md_v2/assets/floating_ask_ai_button.js
    new file mode 100644
    index 00000000..177c2356
    --- /dev/null
    +++ b/docs/md_v2/assets/floating_ask_ai_button.js
    @@ -0,0 +1,39 @@
    +// ==== File: docs/assets/floating_ask_ai_button.js ====
    +
    +document.addEventListener('DOMContentLoaded', () => {
    +    const askAiPagePath = '/core/ask-ai/'; // IMPORTANT: Adjust this path if needed!
    +    const currentPath = window.location.pathname;
    +
    +    // Determine the base URL for constructing the link correctly,
    +    // especially if deployed in a sub-directory.
    +    // This assumes a simple structure; adjust if needed.
    +    const baseUrl = window.location.origin + (currentPath.startsWith('/core/') ? '../..' : '');
    +
    +
    +    // Check if the current page IS the Ask AI page
    +    // Use includes() for flexibility (handles trailing slash or .html)
    +    if (currentPath.includes(askAiPagePath.replace(/\/$/, ''))) { // Remove trailing slash for includes check
    +        console.log("Floating Ask AI Button: Not adding button on the Ask AI page itself.");
    +        return; // Don't add the button on the target page
    +    }
    +
    +    // --- Create the button ---
    +    const fabLink = document.createElement('a');
    +    fabLink.className = 'floating-ask-ai-button';
    +    fabLink.href = askAiPagePath; // Construct the correct URL
    +    fabLink.title = 'Ask Crawl4AI Assistant';
    +    fabLink.setAttribute('aria-label', 'Ask Crawl4AI Assistant');
    +
    +    // Add content (using SVG icon for better visuals)
    +    fabLink.innerHTML = `
    +        
    +            
    +        
    +        Ask AI
    +    `;
    +
    +    // Append to body
    +    document.body.appendChild(fabLink);
    +
    +    console.log("Floating Ask AI Button added.");
    +});
    \ No newline at end of file
    diff --git a/docs/md_v2/assets/layout.css b/docs/md_v2/assets/layout.css
    index db5fac55..f8dbedde 100644
    --- a/docs/md_v2/assets/layout.css
    +++ b/docs/md_v2/assets/layout.css
    @@ -72,7 +72,7 @@ body {
     #terminal-mkdocs-side-panel {
         position: fixed;
         top: var(--header-height);
    -    left: max(0px, calc((100vw - var(--content-max-width)) / 2)); 
    +    left: max(0px, calc((90vw - var(--content-max-width)) / 2)); 
         bottom: 0;
         width: var(--sidebar-width);
         background-color: var(--background-color);
    @@ -294,4 +294,148 @@ footer {
          .github-stats-badge {
             display: none; /* Example: Hide completely on smallest screens */
          }
    +}
    +
    +/* --- Ask AI Selection Button --- */
    +.ask-ai-selection-button {
    +    background-color: var(--primary-dimmed-color, #09b5a5);
    +    color: var(--background-color, #070708);
    +    border: none;
    +    padding: 4px 8px;
    +    font-size: 0.8em;
    +    border-radius: 4px;
    +    cursor: pointer;
    +    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
    +    transition: background-color 0.2s ease;
    +    white-space: nowrap;
    +}
    +
    +.ask-ai-selection-button:hover {
    +    background-color: var(--primary-color, #50ffff);
    +}
    +
    +/* ==== File: docs/assets/layout.css (Additions) ==== */
    +
    +/* ... (keep all existing layout CSS) ... */
    +
    +/* --- Copy Code Button Styling --- */
    +
    +/* Ensure the parent 
     can contain the absolutely positioned button */
    +#terminal-mkdocs-main-content pre {
    +    position: relative; /* Needed for absolute positioning of child */
    +    /* Add a little padding top/right to make space for the button */
    +    padding-top: 2.5em;
    +    padding-right: 1em; /* Ensure padding is sufficient */
    +}
    +
    +.copy-code-button {
    +    position: absolute;
    +    top: 0.5em; /* Adjust spacing from top */
    +    left: 0.5em; /* Adjust spacing from left */
    +    z-index: 1; /* Sit on top of code */
    +
    +    background-color: var(--progress-bar-background, #444); /* Use a background */
    +    color: var(--font-color, #eaeaea);
    +    border: 1px solid var(--secondary-color, #727578);
    +    padding: 3px 8px;
    +    font-size: 0.8em;
    +    font-family: var(--font-stack, monospace);
    +    border-radius: 4px;
    +    cursor: pointer;
    +    opacity: 0; /* Hidden by default */
    +    transition: opacity 0.2s ease-in-out, background-color 0.2s ease, color 0.2s ease;
    +    white-space: nowrap;
    +}
    +
    +/* Show button on hover of the 
     container */
    +#terminal-mkdocs-main-content pre:hover .copy-code-button {
    +    opacity: 0.8; /* Show partially */
    +}
    +
    +.copy-code-button:hover {
    +    opacity: 1; /* Fully visible on button hover */
    +    background-color: var(--secondary-color, #727578);
    +}
    +
    +.copy-code-button:focus {
    +     opacity: 1; /* Ensure visible when focused */
    +     outline: 1px dashed var(--primary-color);
    +}
    +
    +
    +/* Style for "Copied!" state */
    +.copy-code-button.copied {
    +    background-color: var(--primary-dimmed-color, #09b5a5);
    +    color: var(--background-color, #070708);
    +    border-color: var(--primary-dimmed-color, #09b5a5);
    +    opacity: 1; /* Ensure visible */
    +}
    +.copy-code-button.copied:hover {
    +     background-color: var(--primary-dimmed-color, #09b5a5); /* Prevent hover change */
    +}
    +
    +/* ==== File: docs/assets/layout.css (Additions) ==== */
    +
    +/* ... (keep all existing layout CSS) ... */
    +
    +/* --- Floating Ask AI Button --- */
    +.floating-ask-ai-button {
    +    position: fixed;
    +    bottom: 25px;
    +    right: 25px;
    +    z-index: 1050; /* Below modals, above most content */
    +
    +    background-color: var(--primary-dimmed-color, #09b5a5);
    +    color: var(--background-color, #070708);
    +    border: none;
    +    border-radius: 50%; /* Make it circular */
    +    width: 60px; /* Adjust size */
    +    height: 60px; /* Adjust size */
    +    padding: 10px; /* Adjust padding */
    +    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
    +    cursor: pointer;
    +    transition: background-color 0.2s ease, transform 0.2s ease;
    +
    +    display: flex;
    +    flex-direction: column; /* Stack icon and text */
    +    align-items: center;
    +    justify-content: center;
    +    text-decoration: none;
    +    text-align: center;
    +}
    +
    +.floating-ask-ai-button svg {
    +    width: 24px; /* Control icon size */
    +    height: 24px;
    +}
    +
    +.floating-ask-ai-button span {
    +    font-size: 0.7em;
    +    margin-top: 2px; /* Space between icon and text */
    +    display: block; /* Ensure it takes space */
    +     line-height: 1;
    +}
    +
    +
    +.floating-ask-ai-button:hover {
    +    background-color: var(--primary-color, #50ffff);
    +    transform: scale(1.05); /* Slight grow effect */
    +}
    +
    +.floating-ask-ai-button:focus {
    +     outline: 2px solid var(--primary-color);
    +     outline-offset: 2px;
    +}
    +
    +/* Optional: Hide text on smaller screens if needed */
    +@media screen and (max-width: 768px) {
    +     .floating-ask-ai-button span {
    +        /* display: none; */ /* Uncomment to hide text */
    +     }
    +     .floating-ask-ai-button {
    +        width: 55px;
    +        height: 55px;
    +        bottom: 20px;
    +        right: 20px;
    +     }
     }
    \ No newline at end of file
    diff --git a/docs/md_v2/assets/selection_ask_ai.js b/docs/md_v2/assets/selection_ask_ai.js
    new file mode 100644
    index 00000000..b5cb471d
    --- /dev/null
    +++ b/docs/md_v2/assets/selection_ask_ai.js
    @@ -0,0 +1,109 @@
    +// ==== File: docs/assets/selection_ask_ai.js ====
    +
    +document.addEventListener('DOMContentLoaded', () => {
    +    let askAiButton = null;
    +    const askAiPageUrl = '/core/ask-ai/'; // Adjust if your Ask AI page path is different
    +
    +    function createAskAiButton() {
    +        const button = document.createElement('button');
    +        button.id = 'ask-ai-selection-btn';
    +        button.className = 'ask-ai-selection-button';
    +        button.textContent = 'Ask AI'; // Or use an icon
    +        button.style.display = 'none'; // Initially hidden
    +        button.style.position = 'absolute';
    +        button.style.zIndex = '1500'; // Ensure it's on top
    +        document.body.appendChild(button);
    +
    +        button.addEventListener('click', handleAskAiClick);
    +        return button;
    +    }
    +
    +    function getSafeSelectedText() {
    +        const selection = window.getSelection();
    +        if (!selection || selection.rangeCount === 0) {
    +            return null;
    +        }
    +        // Avoid selecting text within the button itself if it was somehow selected
    +        const container = selection.getRangeAt(0).commonAncestorContainer;
    +        if (askAiButton && askAiButton.contains(container)) {
    +             return null;
    +        }
    +
    +        const text = selection.toString().trim();
    +        return text.length > 0 ? text : null;
    +    }
    +
    +    function positionButton(event) {
    +         const selection = window.getSelection();
    +         if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
    +             hideButton();
    +             return;
    +         }
    +
    +        const range = selection.getRangeAt(0);
    +        const rect = range.getBoundingClientRect();
    +
    +        // Calculate position: top-right of the selection
    +        const scrollX = window.scrollX;
    +        const scrollY = window.scrollY;
    +        const buttonTop = rect.top + scrollY - askAiButton.offsetHeight - 5; // 5px above
    +        const buttonLeft = rect.right + scrollX + 5; // 5px to the right
    +
    +        askAiButton.style.top = `${buttonTop}px`;
    +        askAiButton.style.left = `${buttonLeft}px`;
    +        askAiButton.style.display = 'block'; // Show the button
    +    }
    +
    +    function hideButton() {
    +        if (askAiButton) {
    +            askAiButton.style.display = 'none';
    +        }
    +    }
    +
    +    function handleAskAiClick(event) {
    +        event.stopPropagation(); // Prevent mousedown from hiding button immediately
    +        const selectedText = getSafeSelectedText();
    +        if (selectedText) {
    +            console.log("Selected Text:", selectedText);
    +            // Base64 encode for URL safety (handles special chars, line breaks)
    +            // Use encodeURIComponent first for proper Unicode handling before btoa
    +            const encodedText = btoa(unescape(encodeURIComponent(selectedText)));
    +            const targetUrl = `${askAiPageUrl}?qq=${encodedText}`;
    +            console.log("Navigating to:", targetUrl);
    +            window.location.href = targetUrl; // Navigate to Ask AI page
    +        }
    +        hideButton(); // Hide after click
    +    }
    +
    +    // --- Event Listeners ---
    +
    +    // Show button on mouse up after selection
    +    document.addEventListener('mouseup', (event) => {
    +        // Slight delay to ensure selection is registered
    +        setTimeout(() => {
    +            const selectedText = getSafeSelectedText();
    +            if (selectedText) {
    +                if (!askAiButton) {
    +                    askAiButton = createAskAiButton();
    +                }
    +                // Don't position if the click was ON the button itself
    +                if (event.target !== askAiButton) {
    +                     positionButton(event);
    +                }
    +            } else {
    +                hideButton();
    +            }
    +        }, 10); // Small delay
    +    });
    +
    +    // Hide button on scroll or click elsewhere
    +    document.addEventListener('mousedown', (event) => {
    +        // Hide if clicking anywhere EXCEPT the button itself
    +        if (askAiButton && event.target !== askAiButton) {
    +            hideButton();
    +        }
    +    });
    +    document.addEventListener('scroll', hideButton, true); // Capture scroll events
    +
    +    console.log("Selection Ask AI script loaded.");
    +});
    \ No newline at end of file
    diff --git a/docs/md_v2/assets/styles.css b/docs/md_v2/assets/styles.css
    index 751aabb7..92e01f85 100644
    --- a/docs/md_v2/assets/styles.css
    +++ b/docs/md_v2/assets/styles.css
    @@ -6,8 +6,8 @@
     }
     
     :root {
    -    --global-font-size: 16px;
    -    --global-code-font-size: 16px;
    +    --global-font-size: 14px;
    +    --global-code-font-size: 13px;
         --global-line-height: 1.5em;
         --global-space: 10px;
         --font-stack: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono,
    @@ -56,7 +56,7 @@
         --toc-width: 240px; /* Adjust based on your desired ToC width */
         --layout-transition-speed: 0.2s; /* For potential future animations */
     
    -    --page-width : 90em; /* Adjust based on your design */
    +    --page-width : 100em; /* Adjust based on your design */
     }
     
     
    diff --git a/docs/md_v2/core/ask-ai.md b/docs/md_v2/core/ask-ai.md
    new file mode 100644
    index 00000000..9122bd29
    --- /dev/null
    +++ b/docs/md_v2/core/ask-ai.md
    @@ -0,0 +1,74 @@
    +
    + +
    + + + + diff --git a/mkdocs.yml b/mkdocs.yml index 1c7be7a3..39e03a88 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,10 +7,11 @@ docs_dir: docs/md_v2 nav: - Home: 'index.md' + - "Ask AI": "core/ask-ai.md" + - "Quick Start": "core/quickstart.md" - Setup & Installation: - "Installation": "core/installation.md" - "Docker Deployment": "core/docker-deployment.md" - - "Quick Start": "core/quickstart.md" - "Blog & Changelog": - "Blog Home": "blog/index.md" - "Changelog": "https://github.com/unclecode/crawl4ai/blob/main/CHANGELOG.md" @@ -86,4 +87,7 @@ extra_javascript: - assets/highlight_init.js - https://buttons.github.io/buttons.js - assets/toc.js - - assets/github_stats.js \ No newline at end of file + - assets/github_stats.js + - assets/selection_ask_ai.js + - assets/copy_code.js + - assets/floating_ask_ai_button.js \ No newline at end of file