// Main content script that injects the AI chat into YouTube import { YoutubeTranscript } from "youtube-transcript"; import { MemoryClient } from "mem0ai"; // Configuration const config = { apiEndpoint: "https://api.openai.com/v1/chat/completions", model: "gpt-4o", chatPosition: "right", // Where to display the chat panel autoExtract: true, // Automatically extract video context mem0ApiKey: "", // Will be set through extension options }; // Initialize Mem0AI - will be initialized properly when API key is available let mem0client = null; let mem0Initializing = false; // Function to initialize Mem0AI with API key from storage async function initializeMem0AI() { if (mem0Initializing) return; // Prevent multiple simultaneous initialization attempts mem0Initializing = true; try { // Get API key from storage const items = await chrome.storage.sync.get(["mem0ApiKey"]); if (items.mem0ApiKey) { try { // Create new client instance with v2.1.11 configuration mem0client = new MemoryClient({ apiKey: items.mem0ApiKey, projectId: "youtube-assistant", // Add a project ID for organization isExtension: true, }); // Set up custom instructions for the YouTube educational assistant await mem0client.updateProject({ custom_instructions: `Your task: Create memories for a YouTube AI assistant. Focus on capturing: 1. User's Knowledge & Experience: - Direct statements about their skills, knowledge, or experience - Their level of expertise in specific areas - Technologies, frameworks, or tools they work with - Their learning journey or background 2. User's Interests & Goals: - What they're trying to learn or understand (user messages may include the video title) - Their specific questions or areas of confusion - Their learning objectives or career goals - Topics they want to explore further 3. Personal Context: - Their current role or position - Their learning style or preferences - Their experience level in the video's topic - Any challenges or difficulties they're facing 4. Video Engagement: - Their reactions to the content - Points they agree or disagree with - Areas they want to discuss further - Connections they make to other topics For each message: - Extract both explicit statements and implicit knowledge - Capture both video-related and personal context - Note any relationships between user's knowledge and video content Remember: The goal is to build a comprehensive understanding of both the user's knowledge and their learning journey through YouTube.`, }); return true; } catch (error) { console.error("Error initializing Mem0AI:", error); return false; } } else { console.log("No Mem0AI API key found in storage"); return false; } } catch (error) { console.error("Error accessing storage:", error); return false; } finally { mem0Initializing = false; } } // Global state let chatState = { messages: [], isVisible: false, isLoading: false, videoContext: null, transcript: null, // Add transcript to state userMemories: null, // Will store retrieved memories currentStreamingMessage: null, // Track the current streaming message }; // Function to extract video ID from YouTube URL function getYouTubeVideoId(url) { const urlObj = new URL(url); const searchParams = new URLSearchParams(urlObj.search); return searchParams.get("v"); } // Function to fetch and log transcript async function fetchAndLogTranscript() { try { // Check if we're on a YouTube video page if ( window.location.hostname.includes("youtube.com") && window.location.pathname.includes("/watch") ) { const videoId = getYouTubeVideoId(window.location.href); if (videoId) { // Fetch transcript using youtube-transcript package const transcript = await YoutubeTranscript.fetchTranscript(videoId); // Decode HTML entities in transcript text const decodedTranscript = transcript.map((entry) => ({ ...entry, text: entry.text .replace(/&#39;/g, "'") .replace(/&quot;/g, '"') .replace(/&lt;/g, "<") .replace(/&gt;/g, ">") .replace(/&amp;/g, "&"), })); // Store transcript in state chatState.transcript = decodedTranscript; } else { return; } } } catch (error) { console.error("Error fetching transcript:", error); chatState.transcript = null; } } // Initialize when the DOM is fully loaded document.addEventListener("DOMContentLoaded", async () => { init(); fetchAndLogTranscript(); await initializeMem0AI(); // Initialize Mem0AI }); // Also attempt to initialize on window load to handle YouTube's SPA behavior window.addEventListener("load", async () => { init(); fetchAndLogTranscript(); await initializeMem0AI(); // Initialize Mem0AI }); // Add another listener for YouTube's navigation events window.addEventListener("yt-navigate-finish", () => { init(); fetchAndLogTranscript(); }); // Main initialization function function init() { // Check if we're on a YouTube page if ( !window.location.hostname.includes("youtube.com") || !window.location.pathname.includes("/watch") ) { return; } // Give YouTube's DOM a moment to settle setTimeout(() => { // Only inject if not already present if (!document.getElementById("ai-chat-assistant-container")) { injectChatInterface(); setupEventListeners(); extractVideoContext(); } }, 1500); } // Extract context from the current YouTube video function extractVideoContext() { if (!config.autoExtract) return; try { const videoTitle = document.querySelector( "h1.title.style-scope.ytd-video-primary-info-renderer" )?.textContent || document.querySelector("h1.title")?.textContent || "Unknown Video"; const channelName = document.querySelector("ytd-channel-name yt-formatted-string") ?.textContent || document.querySelector("ytd-channel-name")?.textContent || "Unknown Channel"; // Video ID from URL const videoId = new URLSearchParams(window.location.search).get("v"); // Update state with basic video context first chatState.videoContext = { title: videoTitle, channel: channelName, videoId: videoId, url: window.location.href, }; } catch (error) { console.error("Error extracting video context:", error); chatState.videoContext = { title: "Error extracting video information", url: window.location.href, }; } } // Inject the chat interface into the YouTube page function injectChatInterface() { // Create main container const container = document.createElement("div"); container.id = "ai-chat-assistant-container"; container.className = "ai-chat-container"; // Set up basic HTML structure container.innerHTML = `
`; // Append to body document.body.appendChild(container); // Add welcome message addMessage( "assistant", "Hello! I can help answer questions about this video. What would you like to know?" ); } // Set up event listeners for the chat interface function setupEventListeners() { // Tab switching const tabs = document.querySelectorAll(".ai-chat-tab"); tabs.forEach((tab) => { tab.addEventListener("click", () => { // Update active tab tabs.forEach((t) => t.classList.remove("active")); tab.classList.add("active"); // Show corresponding content const tabName = tab.dataset.tab; document.getElementById("ai-chat-content").style.display = tabName === "chat" ? "flex" : "none"; document.getElementById("ai-chat-memories").style.display = tabName === "memories" ? "flex" : "none"; // Load memories if switching to memories tab if (tabName === "memories") { loadMemories(); } }); }); // Refresh memories button document .getElementById("refresh-memories") ?.addEventListener("click", loadMemories); // Toggle chat visibility document.getElementById("ai-chat-toggle")?.addEventListener("click", () => { const container = document.getElementById("ai-chat-assistant-container"); chatState.isVisible = !chatState.isVisible; if (chatState.isVisible) { container.classList.add("visible"); } else { container.classList.remove("visible"); } }); // Close button document.getElementById("ai-chat-close")?.addEventListener("click", () => { const container = document.getElementById("ai-chat-assistant-container"); container.classList.remove("visible"); chatState.isVisible = false; }); // Minimize button document.getElementById("ai-chat-minimize")?.addEventListener("click", () => { const container = document.getElementById("ai-chat-assistant-container"); container.classList.toggle("minimized"); }); // Send message on button click document .getElementById("ai-chat-send") ?.addEventListener("click", sendMessage); // Send message on Enter key (but allow Shift+Enter for new lines) document.getElementById("ai-chat-input")?.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // Add click handler for manage memories link document .getElementById("manage-memories-link") .addEventListener("click", (e) => { e.preventDefault(); chrome.runtime.sendMessage({ action: "openOptions" }, (response) => { if (chrome.runtime.lastError) { console.error("Error opening options:", chrome.runtime.lastError); // Fallback: Try to open directly in a new tab chrome.tabs.create({ url: chrome.runtime.getURL("options.html") }); } }); }); } // Add a message to the chat function addMessage(role, text, isStreaming = false) { const messagesContainer = document.getElementById("ai-chat-messages"); if (!messagesContainer) return; const messageElement = document.createElement("div"); messageElement.className = `ai-chat-message ${role}`; // Enhanced markdown-like formatting let formattedText = text // Code blocks .replace(/```([\s\S]*?)```/g, "
$1
") // Inline code .replace(/`([^`]+)`/g, "$1") // Links .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') // Bold text .replace(/\*\*([^*]+)\*\*/g, "$1") // Italic text .replace(/\*([^*]+)\*/g, "$1") // Lists .replace(/^\s*[-*]\s+(.+)$/gm, "
  • $1
  • ") .replace(/(
  • .*<\/li>)/s, "") // Line breaks .replace(/\n/g, "
    "); messageElement.innerHTML = formattedText; messagesContainer.appendChild(messageElement); // Scroll to bottom messagesContainer.scrollTop = messagesContainer.scrollHeight; // Add to messages array if not streaming if (!isStreaming) { chatState.messages.push({ role, content: text }); } return messageElement; } // Format streaming text with markdown function formatStreamingText(text) { return text // Code blocks .replace(/```([\s\S]*?)```/g, "
    $1
    ") // Inline code .replace(/`([^`]+)`/g, "$1") // Links .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') // Bold text .replace(/\*\*([^*]+)\*\*/g, "$1") // Italic text .replace(/\*([^*]+)\*/g, "$1") // Lists .replace(/^\s*[-*]\s+(.+)$/gm, "
  • $1
  • ") .replace(/(
  • .*<\/li>)/s, "") // Line breaks .replace(/\n/g, "
    "); } // Send a message to the AI async function sendMessage() { const inputElement = document.getElementById("ai-chat-input"); if (!inputElement) return; const userMessage = inputElement.value.trim(); if (!userMessage) return; // Clear input inputElement.value = ""; // Add user message to chat addMessage("user", userMessage); // Show loading indicator chatState.isLoading = true; const loadingMessage = document.createElement("div"); loadingMessage.className = "ai-chat-message assistant loading"; loadingMessage.textContent = "Thinking..."; document.getElementById("ai-chat-messages").appendChild(loadingMessage); try { // If mem0client is available, store the message as a memory and search for relevant memories if (mem0client) { try { // Store the message as a memory await mem0client.add( [ { role: "user", content: `${userMessage}\n\nVideo title: ${chatState.videoContext?.title}`, }, ], { user_id: "youtube-assistant-mem0", // Required parameter metadata: { videoId: chatState.videoContext?.videoId || "", videoTitle: chatState.videoContext?.title || "", }, } ); // Search for relevant memories const searchResults = await mem0client.search(userMessage, { user_id: "youtube-assistant-mem0", // Required parameter limit: 5, }); // Store the retrieved memories chatState.userMemories = searchResults || null; } catch (memoryError) { console.error("Error with Mem0AI operations:", memoryError); // Continue with the chat process even if memory operations fail } } // Prepare messages with context (now includes memories if available) const contextualizedMessages = prepareMessagesWithContext(); // Remove loading message document.getElementById("ai-chat-messages").removeChild(loadingMessage); // Create a new message element for streaming chatState.currentStreamingMessage = addMessage("assistant", "", true); // Send to background script to handle API call chrome.runtime.sendMessage( { action: "sendChatRequest", messages: contextualizedMessages, model: config.model, }, (response) => { chatState.isLoading = false; if (response.error) { addMessage("system", `Error: ${response.error}`); } } ); } catch (error) { // Remove loading indicator document.getElementById("ai-chat-messages").removeChild(loadingMessage); chatState.isLoading = false; // Show error addMessage("system", `Error: ${error.message}`); } } // Prepare messages with added context function prepareMessagesWithContext() { const messages = [...chatState.messages]; // If we have video context, add it as system message at the beginning if (chatState.videoContext) { let transcriptSection = ""; // Add transcript if available if (chatState.transcript) { // Format transcript into a readable string const formattedTranscript = chatState.transcript .map((entry) => `${entry.text}`) .join("\n"); transcriptSection = `\n\nTranscript:\n${formattedTranscript}`; } // Add user memories if available let userMemoriesSection = ""; if (chatState.userMemories && chatState.userMemories.length > 0) { const formattedMemories = chatState.userMemories .map((memory) => `${memory.memory}`) .join("\n"); userMemoriesSection = `\n\nUser Memories:\n${formattedMemories}\n\n`; } const systemContent = `You are an AI assistant helping with a YouTube video. Here's the context: Title: ${chatState.videoContext.title} Channel: ${chatState.videoContext.channel} URL: ${chatState.videoContext.url} ${ userMemoriesSection ? `Use the user memories below to personalize your response based on their past interactions and interests. These memories represent relevant past conversations and information about the user. ${userMemoriesSection} ` : "" } Please provide helpful, relevant information based on the video's content. ${ transcriptSection ? `"Use the transcript below to provide accurate answers about the video. Ignore if the transcript doesn't make sense." ${transcriptSection} ` : "Since the transcript is not available, focus on general questions about the topic and use the video title for context. If asked about specific parts of the video content, politely explain that the video doesn't have a transcript." } Be concise and helpful in your responses. `; messages.unshift({ role: "system", content: systemContent, }); } return messages; } // Listen for commands from the background script or popup chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "toggleChat") { const container = document.getElementById("ai-chat-assistant-container"); chatState.isVisible = !chatState.isVisible; if (chatState.isVisible) { container.classList.add("visible"); } else { container.classList.remove("visible"); } sendResponse({ success: true }); } else if (message.action === "streamChunk") { // Handle streaming chunks if (chatState.currentStreamingMessage) { const currentContent = chatState.currentStreamingMessage.innerHTML; chatState.currentStreamingMessage.innerHTML = formatStreamingText(currentContent + message.chunk); // Scroll to bottom const messagesContainer = document.getElementById("ai-chat-messages"); messagesContainer.scrollTop = messagesContainer.scrollHeight; } } }); // Load memories from mem0 async function loadMemories() { try { const memoriesContainer = document.getElementById("memories-list"); memoriesContainer.innerHTML = '
    Loading memories...
    '; // If client isn't initialized, try to initialize it if (!mem0client) { const initialized = await initializeMem0AI(); if (!initialized) { memoriesContainer.innerHTML = '
    Please set your Mem0 API key in the extension options.
    '; return; } } const response = await mem0client.getAll({ user_id: "youtube-assistant-mem0", page: 1, page_size: 50, }); if (response && response.results) { memoriesContainer.innerHTML = ""; response.results.forEach((memory) => { const memoryElement = document.createElement("div"); memoryElement.className = "memory-item"; memoryElement.textContent = memory.memory; memoriesContainer.appendChild(memoryElement); }); if (response.results.length === 0) { memoriesContainer.innerHTML = '
    No memories found
    '; } } else { memoriesContainer.innerHTML = '
    No memories found
    '; } } catch (error) { console.error("Error loading memories:", error); document.getElementById("memories-list").innerHTML = '
    Error loading memories. Please try again.
    '; } }