mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-05-07 18:18:42 -05:00
736 lines
25 KiB
JavaScript
736 lines
25 KiB
JavaScript
import { openDB } from "idb"; // Ensure you have `idb` library installed or included
|
|
import { copy_download } from "../../components/settings/copy_download.js";
|
|
|
|
const copy_buttons = `
|
|
|
|
<div id="utility-btn-container" class="text-blue-400 flex space-x-2 p-2">
|
|
<!-- <button id="reference-btn" class="bg-blue-100 text-zinc-900 px-4 py-0.5 rounded-md">1</button> -->
|
|
<!-- Buttons with icons -->
|
|
|
|
<button id='highlight-btn' class="w-4 h-4 flex items-center justify-center hover:text-blue-700"
|
|
title="Highlight">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="icons">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
|
</svg>
|
|
</button>
|
|
</button>
|
|
|
|
<button id='sr-send-btn' class="w-4 h-4 mr-2 flex items-center justify-center hover:text-blue-700 st-send-btn"
|
|
>
|
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="icons">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z" />
|
|
</svg>
|
|
|
|
</button>
|
|
<button id='share-btn' class="w-4 h-4 flex items-center justify-center hover:text-blue-700" title="Share"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="icons">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z" />
|
|
</svg>
|
|
</button>
|
|
<button id='copy-btn' class="w-4 h-4 mr-2 flex items-center justify-center hover:text-blue-700" title="Copy"><svg id="copy-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="icons">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
|
</svg>
|
|
</button>
|
|
<button id='download-btn'class="w-4 h-4 mr-2 flex items-center justify-center hover:text-blue-700" title="Download"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="icons">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
|
</svg>
|
|
|
|
</div>`;
|
|
|
|
// const button_util_container = `
|
|
// <div class="flex justify-between"> </div>
|
|
// `
|
|
|
|
export function debounce(func, wait) {
|
|
let timeout;
|
|
return function (...args) {
|
|
const context = this;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => func.apply(context, args), wait);
|
|
};
|
|
}
|
|
|
|
export function extractQuestion(text) {
|
|
// Remove leading/trailing whitespace
|
|
text = text.trim();
|
|
|
|
// Remove "Use this question:" and variants
|
|
text = text.replace(/^(Use this question:?\s*)/i, '');
|
|
|
|
// Case 1: Text between * and ?
|
|
if (text.startsWith('*')) {
|
|
const match = text.match(/\*(.*?)\?/);
|
|
if (match) return match[1].trim() + '?';
|
|
}
|
|
|
|
// Case 2: Text up to first ?
|
|
const questionMatch = text.match(/(.*?)\?/);
|
|
if (questionMatch) return questionMatch[0].trim();
|
|
|
|
// Case 3: Return full text if no ? found
|
|
return text;
|
|
}
|
|
|
|
export function generateUniqueId() {
|
|
// Get current timestamp in milliseconds
|
|
const timestamp = new Date().getTime();
|
|
|
|
// Generate a random number and convert to base 36 (includes letters)
|
|
const randomPart = Math.random().toString(36).substring(2, 10);
|
|
|
|
// Get performance timing if available for more precision
|
|
const performance =
|
|
window.performance && window.performance.now
|
|
? window.performance.now().toString(36).replace(".", "")
|
|
: "";
|
|
|
|
// Combine all parts
|
|
return `id-${timestamp}-${performance}-${randomPart}`;
|
|
}
|
|
|
|
// We can also add a simpler version if needed
|
|
export function generateSimpleId() {
|
|
return `${new Date().getTime()}-${Math.random()
|
|
.toString(36)
|
|
.substring(2, 15)}`;
|
|
}
|
|
|
|
export function truncateToLastChars(text, maxChars) {
|
|
if (text.length <= maxChars) return text;
|
|
return text.substr(text.length - maxChars);
|
|
}
|
|
export function alert(shadowElem, text, type) {
|
|
const token = "none";
|
|
try {
|
|
let noticeElement;
|
|
if (type === "success") {
|
|
noticeElement = shadowElem.querySelector("#success-notice");
|
|
if (!noticeElement) {
|
|
// Create the success notice element
|
|
noticeElement = document.createElement("div");
|
|
noticeElement.id = "success-notice";
|
|
noticeElement.style.display = "none";
|
|
noticeElement.style.position = "fixed";
|
|
noticeElement.style.bottom = "5px";
|
|
noticeElement.style.left = "5px";
|
|
noticeElement.style.background = "lightgreen";
|
|
noticeElement.style.padding = "5px";
|
|
noticeElement.style.borderRadius = "5px";
|
|
noticeElement.style.zIndex = "9999";
|
|
|
|
shadowElem.appendChild(noticeElement);
|
|
}
|
|
noticeElement.textContent = text;
|
|
noticeElement.style.display = "block";
|
|
|
|
// Hide the success notice after 3 seconds
|
|
setTimeout(() => {
|
|
noticeElement.style.display = "none";
|
|
}, 2000);
|
|
return token;
|
|
} else {
|
|
noticeElement = shadowElem.querySelector("#error-notice");
|
|
if (!noticeElement) {
|
|
// Create the error notice element
|
|
noticeElement = document.createElement("div");
|
|
noticeElement.id = "error-notice";
|
|
noticeElement.style.display = "none";
|
|
noticeElement.style.position = "fixed";
|
|
noticeElement.style.bottom = "5px";
|
|
noticeElement.style.left = "5px";
|
|
noticeElement.style.background = "lightcoral";
|
|
noticeElement.style.padding = "5px";
|
|
noticeElement.style.borderRadius = "5px";
|
|
noticeElement.style.zIndex = "9999";
|
|
shadowElem.appendChild(noticeElement);
|
|
}
|
|
noticeElement.textContent = text;
|
|
noticeElement.style.display = "block";
|
|
|
|
// Hide the error notice after 3 seconds
|
|
setTimeout(() => {
|
|
noticeElement.style.display = "none";
|
|
}, 2000);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error in alert function:", error);
|
|
}
|
|
}
|
|
|
|
export async function saveToken(token) {
|
|
let db;
|
|
try {
|
|
db = await openDB("tinyML_token", 1, {
|
|
upgrade(db) {
|
|
if (!db.objectStoreNames.contains("tokens")) {
|
|
db.createObjectStore("tokens", { keyPath: "id" });
|
|
}
|
|
},
|
|
});
|
|
const tx = db.transaction("tokens", "readwrite");
|
|
const store = tx.objectStore("tokens");
|
|
await store.put({ id: "token_avaya", value: token });
|
|
await tx.done; // Ensures the transaction completes successfully
|
|
} catch (error) {
|
|
console.error("Failed to save token:", error);
|
|
// Handle the error (e.g., by retrying, logging, or notifying the user)
|
|
} finally {
|
|
if (db) db.close(); // Close the database connection to prevent leaks
|
|
}
|
|
}
|
|
|
|
export async function getToken() {
|
|
const db = await openDB("tinyML_token", 1);
|
|
const tx = db.transaction("tokens", "readonly");
|
|
const store = tx.objectStore("tokens");
|
|
const tokenObject = await store.get("token_avaya");
|
|
await tx.done;
|
|
return tokenObject ? tokenObject.value : null;
|
|
}
|
|
|
|
export function scrollToBottom(shadowRoot) {
|
|
const iframeContainer = shadowRoot.querySelector("#message-container");
|
|
|
|
if (iframeContainer) {
|
|
// Wait for the next 'paint' after the browser has had a chance to layout content
|
|
requestAnimationFrame(() => {
|
|
iframeContainer.scrollTop = iframeContainer.scrollHeight;
|
|
});
|
|
}
|
|
}
|
|
|
|
export function scrollToBottomSmooth(shadowRoot) {
|
|
const iframeContainer = shadowRoot.querySelector("#message-container");
|
|
let scrollTarget = shadowRoot.querySelector("#scroll-target");
|
|
|
|
if (iframeContainer && scrollTarget) {
|
|
scrollTarget.scrollIntoView({ behavior: "smooth" });
|
|
} else if (iframeContainer && !scrollTarget) {
|
|
// Create a new element with the ID 'scroll-target'
|
|
scrollTarget = document.createElement("div");
|
|
scrollTarget.id = "scroll-target";
|
|
// Append the new element to the iframeContainer
|
|
iframeContainer.appendChild(scrollTarget);
|
|
// Now scroll into view
|
|
scrollTarget.scrollIntoView({ behavior: "smooth" });
|
|
}
|
|
}
|
|
|
|
export function scrollToTopSmooth(shadowRoot) {
|
|
const iframeContainer = shadowRoot.querySelector("#message-container");
|
|
console.log("we gonna SMOOTH scroll down");
|
|
|
|
// Get ALL difficulty-dropdown elements and select the last one
|
|
const difficultyDropdowns = iframeContainer.querySelectorAll(".difficulty-dropdown");
|
|
const lastDifficultyDropdown = difficultyDropdowns[difficultyDropdowns.length - 1];
|
|
|
|
// Scroll the last difficultyDropdown into view if it exists
|
|
if (lastDifficultyDropdown) {
|
|
lastDifficultyDropdown.scrollIntoView({ behavior: "smooth", block: 'start' });
|
|
console.log("Scrolling to last difficulty dropdown:", lastDifficultyDropdown);
|
|
} else {
|
|
console.log("No difficulty dropdowns found");
|
|
}
|
|
}
|
|
|
|
export function cloneElementById(
|
|
shadowEle,
|
|
originalElementId,
|
|
newId = "",
|
|
targetContainerId = ""
|
|
) {
|
|
|
|
const originalElement = shadowEle.querySelector(`#${originalElementId}`);
|
|
if (!originalElement) {
|
|
console.error("Element with ID " + originalElementId + " not found.");
|
|
return null;
|
|
}
|
|
|
|
// Clone the original element
|
|
const clone = originalElement.cloneNode(true);
|
|
|
|
// Assign a new ID to the clone if provided
|
|
if (newId !== "") {
|
|
clone.id = newId;
|
|
} else {
|
|
clone.removeAttribute("id"); // Remove ID attribute if no new ID provided
|
|
}
|
|
|
|
// If a target container ID is provided, append the clone to this container
|
|
if (targetContainerId !== "") {
|
|
const targetContainer = shadowEle.querySelector(`#${targetContainerId}`);
|
|
|
|
if (targetContainer) {
|
|
if (targetContainerId === "message-container") {
|
|
const scrollTarget = shadowEle.querySelector("#scroll-target");
|
|
|
|
// Insert the new message just before the scroll-target
|
|
targetContainer.insertBefore(clone, scrollTarget);
|
|
} else {
|
|
// if (targetContainer) {
|
|
targetContainer.appendChild(clone);
|
|
}
|
|
} else {
|
|
console.error(
|
|
"Target container with ID " + targetContainerId + " not found."
|
|
);
|
|
}
|
|
}
|
|
|
|
// Return the cloned element
|
|
return clone;
|
|
}
|
|
|
|
export function cloneElementById_OLD_DELETE(
|
|
shadowEle,
|
|
originalElementId,
|
|
newId = "",
|
|
targetContainerId = ""
|
|
) {
|
|
// Assuming shadowEle is a shadow root or document
|
|
const originalElement = shadowEle.querySelector(`#${originalElementId}`);
|
|
if (!originalElement) {
|
|
console.error("Element with ID " + originalElementId + " not found.");
|
|
return null;
|
|
}
|
|
|
|
// Clone the original element
|
|
const clone = originalElement.cloneNode(true);
|
|
|
|
// Assign a new ID to the clone if provided
|
|
if (newId !== "") {
|
|
clone.id = newId;
|
|
} else {
|
|
clone.removeAttribute("id"); // Remove ID attribute if no new ID provided
|
|
}
|
|
|
|
// If a target container ID is provided, append the clone to this container
|
|
if (targetContainerId !== "") {
|
|
const targetContainer = shadowEle.querySelector(`#${targetContainerId}`);
|
|
if (targetContainer) {
|
|
targetContainer.appendChild(clone);
|
|
} else {
|
|
console.error(
|
|
"Target container with ID " + targetContainerId + " not found."
|
|
);
|
|
}
|
|
}
|
|
|
|
// Return the cloned element
|
|
return clone;
|
|
}
|
|
|
|
export function generateHighPrecisionUniqueId() {
|
|
const perfTime = performance.now().toString().replace(".", "");
|
|
const randomPortion = Math.random().toString(36).substring(2, 15);
|
|
return `id-${perfTime}-${randomPortion}`;
|
|
}
|
|
|
|
export function assignUniqueIdsToElementAndChildren(element) {
|
|
// Assign a unique ID to the root element
|
|
element.id = generateHighPrecisionUniqueId();
|
|
|
|
// Iterate over all child elements and recursively assign unique IDs
|
|
element.querySelectorAll("*").forEach(assignUniqueIdsToElementAndChildren);
|
|
}
|
|
|
|
export function assignAndTrackUniqueIds(element, idsToTrack = []) {
|
|
// Initialize array to store new IDs in same order as idsToTrack
|
|
const trackedNewIds = new Array(idsToTrack.length).fill(null);
|
|
|
|
// Assign unique ID to root element
|
|
const rootId = generateHighPrecisionUniqueId();
|
|
element.id = rootId;
|
|
|
|
// Check if root element's original ID was in tracking list
|
|
const rootIndex = idsToTrack.indexOf(element.id);
|
|
if (rootIndex !== -1) {
|
|
trackedNewIds[rootIndex] = rootId;
|
|
}
|
|
|
|
// Process all child elements
|
|
element.querySelectorAll("*").forEach(child => {
|
|
const originalId = child.id;
|
|
const newId = generateHighPrecisionUniqueId();
|
|
child.id = newId;
|
|
|
|
// If this child's original ID was in our tracking list, store the new ID
|
|
const trackIndex = idsToTrack.indexOf(originalId);
|
|
if (trackIndex !== -1) {
|
|
trackedNewIds[trackIndex] = newId;
|
|
}
|
|
});
|
|
|
|
return trackedNewIds;
|
|
}
|
|
|
|
// Add this new function to encode messages for URL
|
|
export function encodeMessageForURL(text) {
|
|
try {
|
|
// If text is an HTML element, get its outerHTML
|
|
const htmlContent = typeof text === "string" ? text : text.outerHTML;
|
|
return encodeURIComponent(htmlContent).replace(/%20/g, "+");
|
|
} catch (error) {
|
|
console.error("Error encoding message for URL:", error);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
// Add this new function to decode messages from URL
|
|
export function decodeMessageFromURL(text) {
|
|
try {
|
|
// First decode the URI component
|
|
const decodedText = decodeURIComponent(text.replace(/\+/g, " "));
|
|
|
|
// Parse the HTML string into actual DOM elements
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(decodedText, "text/html");
|
|
|
|
// Return the first element from the body
|
|
// This will preserve the entire HTML structure
|
|
return doc.body.firstElementChild;
|
|
} catch (error) {
|
|
console.error("Error decoding message from URL:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Add new popover function
|
|
export function showPopover(shadowEle, message, type = "info", duration = 3000) {
|
|
const popover = document.createElement('div');
|
|
popover.style.cssText = `
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
padding: 12px 24px;
|
|
border-radius: 8px;
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
transition: all 0.3s ease;
|
|
z-index: 10000;
|
|
font-size: 14px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
`;
|
|
popover.classList.add('popover-socratiq');
|
|
|
|
// Set style based on type
|
|
switch(type) {
|
|
case "info":
|
|
popover.style.backgroundColor = 'rgba(59, 130, 246, 0.9)'; // Blue
|
|
popover.style.color = 'white';
|
|
break;
|
|
case "error":
|
|
popover.style.backgroundColor = 'rgba(239, 68, 68, 0.9)'; // Red
|
|
popover.style.color = 'white';
|
|
break;
|
|
case "success":
|
|
popover.style.backgroundColor = 'rgba(34, 197, 94, 0.9)'; // Green
|
|
popover.style.color = 'white';
|
|
break;
|
|
default:
|
|
popover.style.backgroundColor = 'rgba(59, 130, 246, 0.9)'; // Default blue
|
|
popover.style.color = 'white';
|
|
}
|
|
|
|
popover.textContent = message;
|
|
shadowEle.appendChild(popover);
|
|
|
|
// Trigger animation
|
|
setTimeout(() => {
|
|
popover.style.opacity = "1";
|
|
popover.style.transform = "translateY(0)";
|
|
}, 10);
|
|
|
|
// Remove after duration
|
|
setTimeout(() => {
|
|
popover.style.opacity = "0";
|
|
popover.style.transform = "translateY(-20px)";
|
|
setTimeout(() => popover.remove(), 300);
|
|
}, duration);
|
|
}
|
|
|
|
// Update share button code
|
|
export function add_copy_paste_share_buttons(
|
|
shadowEle,
|
|
clone,
|
|
ref_buttons_element,
|
|
accumulatedResponse = ""
|
|
) {
|
|
// Store the markdown content as a data attribute
|
|
if (accumulatedResponse) {
|
|
clone.setAttribute("data-markdown", accumulatedResponse);
|
|
}
|
|
|
|
const container = document.createElement("div");
|
|
container.setAttribute("class", "flex justify-between");
|
|
container.classList.add("copy-paste-share-btn-container");
|
|
|
|
|
|
const refButtonsContainer = document.createElement("div");
|
|
refButtonsContainer.setAttribute("class", "flex items-center");
|
|
|
|
if (ref_buttons_element instanceof Element) {
|
|
refButtonsContainer.appendChild(ref_buttons_element);
|
|
}
|
|
|
|
const copyButtonsContainer = document.createElement("div");
|
|
copyButtonsContainer.innerHTML = copy_buttons;
|
|
|
|
container.appendChild(refButtonsContainer);
|
|
container.appendChild(copyButtonsContainer);
|
|
|
|
|
|
clone.appendChild(container);
|
|
|
|
copy_download(shadowEle, clone);
|
|
|
|
return clone;
|
|
}
|
|
|
|
/**
|
|
* Sets the target attribute of all <a> tags inside <td> elements to '_blank' and adds rel='noopener noreferrer'.
|
|
*
|
|
* @param {HTMLElement} clone - The element to clone and search for <a> tags
|
|
*/
|
|
export function make_links_load_new_page(clone) {
|
|
// Query all <a> tags inside <td> elements
|
|
const links = clone.querySelectorAll("a");
|
|
|
|
// Set the target attribute of each link to '_blank'
|
|
links.forEach((link) => {
|
|
link.setAttribute("target", "_blank");
|
|
// Optional: Add rel="noopener noreferrer" for security reasons
|
|
link.setAttribute("rel", "noopener noreferrer");
|
|
});
|
|
}
|
|
|
|
|
|
export function updateCloneAttributes(clone, prompt, type, title='') {
|
|
// Ensure the clone is a valid DOM element
|
|
if (!(clone instanceof HTMLElement)) {
|
|
throw new Error("Invalid clone element provided.");
|
|
}
|
|
|
|
|
|
clone.setAttribute('data-prompt', prompt);
|
|
clone.setAttribute('data-type', type);
|
|
clone.setAttribute('data-title', title);
|
|
}
|
|
|
|
|
|
export function removeSkeletonLoaders(clone) {
|
|
// Remove all skeleton loader elements
|
|
const skeletonLoaders = clone.querySelectorAll('.skeleton-loader');
|
|
skeletonLoaders.forEach(loader => loader.remove());
|
|
|
|
// Also remove any elements with loader class
|
|
const loaderElements = clone.querySelectorAll('.loader');
|
|
loaderElements.forEach(loader => loader.remove());
|
|
|
|
return clone;
|
|
}
|
|
|
|
|
|
export function insertDiagramElements(messageContent, clone) {
|
|
// Find all strong elements
|
|
const strongElements = clone.querySelectorAll('strong');
|
|
|
|
// Find the one containing "Related" text
|
|
const relatedElement = Array.from(strongElements).find(el =>
|
|
el.textContent.toLowerCase().includes('related')
|
|
);
|
|
|
|
if (relatedElement) {
|
|
// Create new div for diagram
|
|
const diagramDiv = document.createElement('div');
|
|
diagramDiv.className = 'generated-diagram';
|
|
diagramDiv.style.cssText = `
|
|
margin: 1.5rem 0;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
background-color: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
`;
|
|
|
|
diagramDiv.innerHTML = `
|
|
<h3 style="margin-bottom: 1rem; font-weight: 600; color: #1f2937;">Generated Diagram</h3>
|
|
${messageContent}
|
|
`;
|
|
|
|
// Insert before the parent element containing "related"
|
|
relatedElement.parentElement.parentElement.insertBefore(
|
|
diagramDiv,
|
|
relatedElement.parentElement
|
|
);
|
|
} else {
|
|
// If no "Related" section found, append to the end of clone
|
|
clone.appendChild(diagramDiv);
|
|
}
|
|
}
|
|
|
|
export function findClosestAIMessage(element) {
|
|
if (!element) {
|
|
console.warn('No element provided to findClosestAIMessage');
|
|
return null;
|
|
}
|
|
|
|
// Try to find the closest parent with any of these selectors
|
|
const selectors = [
|
|
'.ai-message-chat',
|
|
'.message-container[data-type="ai"]',
|
|
'[id^="id-"][class*="text-sm"][class*="markdown-preview-container"]',
|
|
'.markdown-preview-container'
|
|
];
|
|
|
|
for (const selector of selectors) {
|
|
const match = element.closest(selector);
|
|
if (match) {
|
|
// For debugging
|
|
console.log(`Found AI message container with selector: ${selector}`);
|
|
return match;
|
|
}
|
|
}
|
|
|
|
// If no match is found, log the element for debugging
|
|
console.warn('Could not find parent AI message element. Element:', element);
|
|
return null;
|
|
}
|
|
|
|
export function insertAtProportionalPoints(parentElement, firstComponent, secondComponent) {
|
|
try {
|
|
// Log initial state
|
|
console.log('Inserting charts - Initial container state:', {
|
|
parentElement,
|
|
childCount: parentElement.children.length,
|
|
firstComponent,
|
|
secondComponent
|
|
});
|
|
|
|
// Get all child nodes that aren't charts (to avoid counting previously inserted charts)
|
|
const children = Array.from(parentElement.children).filter(child =>
|
|
!child.classList.contains('chart-wrapper')
|
|
);
|
|
const totalChildren = children.length;
|
|
|
|
console.log('Filtered children count:', totalChildren);
|
|
|
|
// Calculate insertion points (rounded to nearest integer)
|
|
const oneThirdIndex = Math.round(totalChildren / 3);
|
|
const twoThirdsIndex = Math.round((totalChildren * 2) / 3);
|
|
|
|
console.log('Calculated insertion points:', {
|
|
oneThirdIndex,
|
|
twoThirdsIndex
|
|
});
|
|
|
|
// Create wrapper divs for the charts
|
|
const firstWrapper = document.createElement('div');
|
|
firstWrapper.className = 'chart-wrapper xy-chart-container';
|
|
firstWrapper.style.cssText = `
|
|
margin: 1.5rem 0;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
background-color: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
`;
|
|
firstWrapper.appendChild(firstComponent);
|
|
|
|
const secondWrapper = document.createElement('div');
|
|
secondWrapper.className = 'chart-wrapper quadrant-chart-container';
|
|
secondWrapper.style.cssText = `
|
|
margin: 1.5rem 0;
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
background-color: #f8f9fa;
|
|
border: 1px solid #e9ecef;
|
|
`;
|
|
secondWrapper.appendChild(secondComponent);
|
|
|
|
// Store references to elements at insertion points
|
|
const firstInsertionPoint = children[oneThirdIndex];
|
|
const secondInsertionPoint = children[twoThirdsIndex];
|
|
|
|
console.log('Insertion points found:', {
|
|
firstInsertionPoint,
|
|
secondInsertionPoint
|
|
});
|
|
|
|
// Insert components at calculated positions
|
|
if (firstInsertionPoint) {
|
|
firstInsertionPoint.parentNode.insertBefore(firstWrapper, firstInsertionPoint);
|
|
console.log('Inserted first chart before:', firstInsertionPoint);
|
|
} else {
|
|
parentElement.appendChild(firstWrapper);
|
|
console.log('Appended first chart to end (no insertion point found)');
|
|
}
|
|
|
|
// Recalculate children after first insertion to maintain proper spacing
|
|
const updatedChildren = Array.from(parentElement.children).filter(child =>
|
|
!child.classList.contains('chart-wrapper')
|
|
);
|
|
const newSecondInsertionPoint = updatedChildren[twoThirdsIndex];
|
|
|
|
if (newSecondInsertionPoint) {
|
|
newSecondInsertionPoint.parentNode.insertBefore(secondWrapper, newSecondInsertionPoint);
|
|
console.log('Inserted second chart before:', newSecondInsertionPoint);
|
|
} else {
|
|
parentElement.appendChild(secondWrapper);
|
|
console.log('Appended second chart to end (no insertion point found)');
|
|
}
|
|
|
|
console.log('Final container state:', {
|
|
childCount: parentElement.children.length,
|
|
chartWrappers: parentElement.querySelectorAll('.chart-wrapper').length
|
|
});
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Error inserting components at proportional points:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function injectOfflineWarning(clone) {
|
|
const markdownContainer = clone.querySelector('.markdown-preview-container');
|
|
if (!markdownContainer) return;
|
|
|
|
// Create warning element
|
|
const warningDiv = document.createElement('div');
|
|
warningDiv.className = 'offline-warning';
|
|
warningDiv.style.cssText = `
|
|
background-color: #fff3cd;
|
|
border: 1px solid #ffeeba;
|
|
border-left: 4px solid #ffc107;
|
|
border-radius: 4px;
|
|
padding: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
font-size: 0.95rem;
|
|
color: #856404;
|
|
`;
|
|
|
|
// Create warning content
|
|
warningDiv.innerHTML = `
|
|
<div style="
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.25rem;
|
|
">
|
|
<span style="font-size: 1.25rem;">⚠️</span>
|
|
<strong>Offline Mode</strong>
|
|
</div>
|
|
<div style="margin-left: 1.75rem;">
|
|
This quiz was generated offline. Some features may be limited.
|
|
</div>
|
|
`;
|
|
|
|
// Insert at the beginning of the markdown container
|
|
markdownContainer.insertBefore(warningDiv, markdownContainer.firstChild);
|
|
}
|
|
|
|
// Add this new function
|
|
export function cleanupPopovers(shadowEle) {
|
|
const popovers = shadowEle.querySelectorAll('.popover-socratiq');
|
|
popovers.forEach(popover => popover.remove());
|
|
} |