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 = '
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