mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-05-10 15:49:25 -05:00
434 lines
14 KiB
JavaScript
434 lines
14 KiB
JavaScript
import { getDBInstance, resetSocratiqDB } from '../../libs/utils/indexDb.js';
|
|
import { loadChat,deleteChat, clearAllRows} from "../../libs/utils/save_chats";
|
|
import {alert, scrollToBottomSmooth} from "../../libs/utils/utils.js";
|
|
import { enableTooltip } from '../tooltip/tooltip.js';
|
|
|
|
const prev_chats = `
|
|
<div id="popover-container" class="hidden">
|
|
<div class="modal-content bg-transparent p-4 max-w-md w-full rounded-lg">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-lg font-semibold text-gray-800">Previous Conversations</h2>
|
|
<button id="close-btn2" class="p-1.5 rounded-lg text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-colors duration-200">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<input
|
|
id="chat-search"
|
|
class="search-input w-full px-3 py-2 rounded-lg border border-gray-200 focus:outline-none focus:border-blue-400 mb-4 text-gray-600 placeholder-gray-400"
|
|
placeholder="Search conversations..."
|
|
/>
|
|
|
|
<ul id="new-chat" class="mb-3 list-none">
|
|
<li class="transition-all duration-200">
|
|
<button
|
|
class="flex items-center justify-between w-full px-4 py-3 text-left rounded-lg bg-blue-50 hover:bg-blue-100 transition-all duration-200 group"
|
|
>
|
|
<span class="text-blue-600 group-hover:text-blue-700 font-medium">New Chat</span>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.5"
|
|
stroke="currentColor"
|
|
class="w-5 h-5 text-blue-600 group-hover:text-blue-700"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
|
|
</svg>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<ul id="list-container" class="space-y-1 overflow-auto max-h-[350px] pr-1">
|
|
<!-- Chat items will be inserted here -->
|
|
</ul>
|
|
|
|
<div class="mt-4 flex justify-between">
|
|
<button
|
|
id="reset-btn"
|
|
class="flex items-center px-4 py-2 text-yellow-600 bg-yellow-50 rounded-lg hover:bg-yellow-100 transition-colors duration-200"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.5"
|
|
stroke="currentColor"
|
|
class="w-5 h-5 mr-2"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
|
</svg>
|
|
Reset SocratiQ
|
|
</button>
|
|
<button
|
|
id="clear-btn"
|
|
class="flex items-center px-4 py-2 text-red-500 bg-red-50 rounded-lg hover:bg-red-100 transition-colors duration-200"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.5"
|
|
stroke="currentColor"
|
|
class="w-5 h-5 mr-2"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
|
|
</svg>
|
|
Delete all Chats
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
function addItem(shadowEle, entry) {
|
|
const saveList = shadowEle.querySelector("#list-container");
|
|
if (saveList) {
|
|
const item = document.createElement("li");
|
|
item.id = "chat-item";
|
|
item.className = "mb-3 transition-all duration-200 ease-in-out";
|
|
item.innerHTML = `
|
|
<div
|
|
id="chat-${entry.id}"
|
|
class="flex items-center justify-between w-full px-4 py-3 text-left rounded-lg cursor-pointer transition-all duration-200 hover:bg-blue-50 group"
|
|
style="background-color: #fafafa;"
|
|
>
|
|
<span class="text-gray-700 group-hover:text-blue-600 font-medium truncate">${entry.title}</span>
|
|
<div class="flex items-center space-x-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
<div id="load-chat-${entry.id}" class="p-1.5 rounded-lg hover:bg-blue-100 transition-colors duration-200">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.5"
|
|
stroke="currentColor"
|
|
class="w-5 h-5 text-blue-600"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
|
|
</svg>
|
|
</div>
|
|
<div id="delete-chat-${entry.id}" class="p-1.5 rounded-lg hover:bg-red-100 transition-colors duration-200">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke-width="1.5"
|
|
stroke="currentColor"
|
|
class="w-5 h-5 text-red-500"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
const chatItem = item.querySelector(`#chat-${entry.id}`);
|
|
const deleteButton = item.querySelector(`#delete-chat-${entry.id}`);
|
|
// const loadButton = item.querySelector(`#load-chat-${entry.id}`);
|
|
|
|
// Allow clicking anywhere on the item to load the chat
|
|
chatItem.addEventListener("click", (e) => {
|
|
// Prevent triggering when clicking the delete button
|
|
if (!e.target.closest(`#delete-chat-${entry.id}`)) {
|
|
rowClicked(shadowEle, entry.id);
|
|
}
|
|
});
|
|
|
|
deleteButton.addEventListener("click", async (e) => {
|
|
e.stopPropagation(); // Prevent chat loading when deleting
|
|
await deleteChat(entry.id);
|
|
populateListAndShowPopover(shadowEle);
|
|
});
|
|
|
|
saveList.appendChild(item);
|
|
}
|
|
}
|
|
|
|
function triggerResetChat() {
|
|
const event = new CustomEvent("resetAIChat", {
|
|
detail: {
|
|
containerId: "reset", // Pass the ID of the message container
|
|
},
|
|
});
|
|
window.dispatchEvent(event);
|
|
}
|
|
|
|
let prevChats;
|
|
// Function to populate a list with conversations and show popover
|
|
async function populateListAndShowPopover(shadowEle) {
|
|
try {
|
|
// Get database instance
|
|
const dbManager = await getDBInstance();
|
|
if (!dbManager || !dbManager.db) {
|
|
throw new Error('Database not properly initialized');
|
|
}
|
|
|
|
const listContainer = shadowEle.querySelector("#list-container");
|
|
if (!listContainer) {
|
|
throw new Error('List container not found');
|
|
}
|
|
|
|
// Get the store
|
|
const store = dbManager.db
|
|
.transaction(['tinyMLDB_chats'], 'readonly')
|
|
.objectStore('tinyMLDB_chats');
|
|
|
|
// Get all entries
|
|
const entries = await new Promise((resolve, reject) => {
|
|
const request = store.getAll();
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(new Error('Error getting entries'));
|
|
});
|
|
|
|
if (prevChats) {
|
|
if (entries.length === prevChats.length) return; // Entries have not changed
|
|
}
|
|
|
|
listContainer.innerHTML = "";
|
|
entries.forEach((entry, index) => {
|
|
addItem(shadowEle, entry, index);
|
|
});
|
|
|
|
prevChats = entries;
|
|
} catch (error) {
|
|
console.error('Failed to populate list:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Function called when a row/item is clicked
|
|
async function rowClicked(shadowEle, id) {
|
|
try {
|
|
const result = await loadChat(shadowEle, id, "socratiqDB", "tinyMLDB_chats");
|
|
if (result) {
|
|
alert(shadowEle, "Loading chat " + id, "success");
|
|
toggleModal(shadowEle);
|
|
scrollToBottomSmooth(shadowEle);
|
|
} else {
|
|
alert(shadowEle, "Failed to load chat " + id, "error");
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load chat:', error);
|
|
alert(shadowEle, "Error loading chat", "error");
|
|
}
|
|
}
|
|
|
|
const toggleModal = async (shadowEle) => {
|
|
try {
|
|
const modal = shadowEle.querySelector("#popover-container");
|
|
let overlay = shadowEle.querySelector(".overlay-progressive");
|
|
|
|
if (!modal || !overlay) {
|
|
throw new Error('Modal or overlay not found');
|
|
}
|
|
|
|
if (modal.style.display === "block") {
|
|
modal.style.display = "none";
|
|
overlay.style.display = "none";
|
|
} else {
|
|
await populateListAndShowPopover(shadowEle);
|
|
modal.style.display = "block";
|
|
overlay.style.display = "block"; // Show the overlay when the modal is opened
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to toggle modal:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Function to setup modal and integrate settings
|
|
export function setupModal_loadchats(shadowEle) {
|
|
const modal = shadowEle.querySelector("#popover-container");
|
|
const closeBtn = shadowEle.querySelector("#close-btn2");
|
|
const search_chats = shadowEle.querySelector("#chat-search");
|
|
const clearAllButton = shadowEle.querySelector("#clear-btn");
|
|
|
|
// Create overlay inside the shadow DOM
|
|
const overlay = document.createElement('div');
|
|
overlay.classList.add("overlay-progressive");
|
|
overlay.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
z-index: 99999;
|
|
display: none;
|
|
`;
|
|
|
|
// Style the modal container with absolute positioning relative to viewport
|
|
modal.style.cssText = `
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 95%;
|
|
max-width: 32rem;
|
|
background: white;
|
|
z-index: 100000;
|
|
display: none;
|
|
`;
|
|
|
|
// Append overlay to shadow root
|
|
shadowEle.appendChild(overlay);
|
|
|
|
// Event Listeners
|
|
const newChatsBtn = shadowEle.querySelector("#new-chat");
|
|
const initiateLoadChatsBtn = shadowEle.querySelector("#new-chat-btn");
|
|
enableTooltip(newChatsBtn, "Start a new chat or load a previous chat", shadowEle);
|
|
|
|
newChatsBtn.addEventListener("click", () => {
|
|
triggerResetChat();
|
|
toggleModal(shadowEle);
|
|
});
|
|
|
|
initiateLoadChatsBtn?.addEventListener("click", async () => {
|
|
toggleModal(shadowEle);
|
|
});
|
|
|
|
closeBtn?.addEventListener("click", () => {
|
|
toggleModal(shadowEle);
|
|
});
|
|
|
|
// Close on overlay click
|
|
overlay.addEventListener('click', () => {
|
|
toggleModal(shadowEle);
|
|
});
|
|
|
|
if (search_chats) {
|
|
search_chats.addEventListener("keyup", async (event) => {
|
|
await searchAndShow(event.target.value, shadowEle);
|
|
});
|
|
}
|
|
|
|
// Prevent modal from closing when clicking inside it
|
|
modal.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
});
|
|
|
|
// Close on escape key
|
|
window.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape" && modal.style.display === "block") {
|
|
toggleModal(shadowEle);
|
|
}
|
|
});
|
|
|
|
// Add clear button logic
|
|
if (clearAllButton) {
|
|
clearAllButton.addEventListener("click", async () => {
|
|
try {
|
|
await clearAllRows();
|
|
const listContainer = shadowEle.querySelector("#list-container");
|
|
if (listContainer) {
|
|
listContainer.innerHTML = "";
|
|
}
|
|
prevChats = [];
|
|
alert(shadowEle, "All chats have been deleted", "success");
|
|
toggleModal(shadowEle);
|
|
} catch (error) {
|
|
console.error("Error clearing chats:", error);
|
|
alert(shadowEle, "Error clearing chats", "error");
|
|
}
|
|
});
|
|
}
|
|
|
|
const resetButton = shadowEle.querySelector("#reset-btn");
|
|
if (resetButton) {
|
|
resetButton.addEventListener("click", async () => {
|
|
const confirmed = confirm("Are you sure you want to reset SocratiQ? This will clear all data and refresh the application.");
|
|
if (confirmed) {
|
|
try {
|
|
await resetSocratiqDB();
|
|
alert(shadowEle, "SocratiQ has been reset. Refreshing...", "success");
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1500);
|
|
} catch (error) {
|
|
console.error("Error resetting SocratiQ:", error);
|
|
alert(shadowEle, "Error resetting SocratiQ", "error");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Return the cleanup function
|
|
return () => {
|
|
if (overlay && overlay.parentNode) {
|
|
overlay.parentNode.removeChild(overlay);
|
|
}
|
|
};
|
|
}
|
|
|
|
let prevEntries = []
|
|
async function searchAndShow(queryText, shadowEle){
|
|
try {
|
|
const dbManager = await getDBInstance();
|
|
if (!dbManager || !dbManager.db) {
|
|
throw new Error('Database not properly initialized');
|
|
}
|
|
|
|
const store = dbManager.db
|
|
.transaction(['tinyMLDB_chats'], 'readonly')
|
|
.objectStore('tinyMLDB_chats');
|
|
|
|
const entries = await new Promise((resolve, reject) => {
|
|
const request = store.getAll();
|
|
request.onsuccess = () => {
|
|
const allEntries = request.result;
|
|
const filteredEntries = allEntries.filter(entry =>
|
|
entry.html_content.toLowerCase().includes(queryText.toLowerCase())
|
|
);
|
|
resolve(filteredEntries);
|
|
};
|
|
request.onerror = () => reject(new Error('Error getting entries'));
|
|
});
|
|
|
|
if (!arraysEqual(entries, prevEntries)) {
|
|
const saveList = shadowEle.querySelector("#list-container");
|
|
if (saveList) {
|
|
saveList.innerHTML = '';
|
|
entries.forEach((entry) => {
|
|
addItem(shadowEle, entry);
|
|
});
|
|
prevEntries = entries;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to search chats:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function arraysEqual(a, b) {
|
|
if (a === b) return true; // If both are the same instance
|
|
if (a == null || b == null) return false; // If one is null (and the other is not)
|
|
if (a.length !== b.length) return false; // If their lengths are different
|
|
|
|
for (let i = 0; i < a.length; ++i) {
|
|
if (a[i] !== b[i]) return false; // If any corresponding elements are not equal
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
export function injectLoad_chats(shadowEle) {
|
|
const template = document.createElement("template");
|
|
template.innerHTML = prev_chats;
|
|
const load_chats = shadowEle.querySelector("#load_chats");
|
|
|
|
if (load_chats) {
|
|
// Clone the node deeply
|
|
// originalprogressContent = load_chats.cloneNode(true);
|
|
load_chats.innerHTML = ""; // Clear existing content if necessary
|
|
load_chats.appendChild(template.content.cloneNode(true));
|
|
} else {
|
|
console.error("Button menu not found");
|
|
}
|
|
return load_chats;
|
|
}
|