Add YT assistant chrome extension (#2485)

This commit is contained in:
Vir Kothari
2025-04-10 22:14:57 +05:30
committed by GitHub
parent fd065fe9cc
commit 8b789adb15
18 changed files with 3577 additions and 1 deletions

View File

@@ -203,7 +203,8 @@
"examples/mem0-agentic-tool",
"examples/openai-inbuilt-tools",
"examples/mem0-openai-voice-demo",
"examples/email_processing"
"examples/email_processing",
"examples/youtube-assistant"
]
}
]

View File

@@ -47,6 +47,10 @@ Explore how **Mem0** can power real-world applications and bring personalized, i
Add **long-term memory** to ChatGPT, Claude, or Perplexity via the **Mem0 Chrome Extension** — personalize your AI chats anywhere.
</Card>
<Card title="YouTube Assistant" icon="puzzle-piece" href="/examples/youtube-assistant">
Integrate **Mem0** into **YouTube's** native UI, providing personalized responses with video context.
</Card>
<Card title="Document Writing Assistant" icon="pen" href="/examples/document-writing">
Create a **Writing Assistant** that understands and adapts to your unique style, improving consistency and productivity.
</Card>

View File

@@ -0,0 +1,56 @@
---
title: YouTube Assistant Extension
---
Enhance your YouTube experience with Mem0's **YouTube Assistant**, a Chrome extension that brings AI-powered chat directly to your YouTube videos. Get instant, personalized answers about video content while leveraging your own knowledge and memories - all without leaving the page.
## Features
- **Contextual AI Chat**: Ask questions about videos you're watching
- **Seamless Integration**: Chat interface sits alongside YouTube's native UI
- **Memory Integration**: Personalized responses based on your knowledge through Mem0
- **Real-Time Memory**: Memories are updated in real-time based on your interactions
## Installation
This extension is not available on the Chrome Web Store yet. You can install it manually using below method:
### Manual Installation (Developer Mode)
1. **Download the Extension**: Clone or download the extension files from the [Mem0 GitHub repository](https://github.com/mem0ai/mem0/tree/main/examples).
2. **Build**: Run `npm install` followed by `npm run build` to install the dependencies and build the extension.
3. **Access Chrome Extensions**: Open Google Chrome and navigate to `chrome://extensions`.
4. **Enable Developer Mode**: Toggle the "Developer mode" switch in the top right corner.
5. **Load Unpacked Extension**: Click "Load unpacked" and select the directory containing the extension files.
6. **Confirm Installation**: The Mem0 YouTube Assistant Extension should now appear in your Chrome toolbar.
## Setup
1. **Configure API Settings**: Click the extension icon and enter your OpenAI API key (required to use the extension)
2. **Customize Settings**: Configure additional settings such as model, temperature, and memory settings
3. **Navigate to YouTube**: Start using the assistant on any YouTube video
4. **Memories**: Enter your Mem0 API key to enable personalized responses, and feed initial memories from settings
## Demo Video
<video
autoPlay
muted
loop
playsInline
width="700"
height="400"
src="https://github.com/user-attachments/assets/c0334ccd-311b-4dd7-8034-ef88204fc751"
></video>
## Example Prompts
- "Can you summarize the main points of this video?"
- "Explain the concept they just mentioned"
- "How does this relate to what I already know?"
- "What are some practical applications of this topic related to my work?"
## Privacy and Data Security
Your API keys are stored locally in your browser. Your messages are sent to the Mem0 API for extracting and retrieving memories. Mem0 is committed to ensuring your data's privacy and security.

View File

@@ -0,0 +1,4 @@
node_modules
.env*
dist
package-lock.json

View File

@@ -0,0 +1,88 @@
# Mem0 Assistant Chrome Extension
A powerful Chrome extension that combines AI chat with your personal knowledge base through mem0. Get instant, personalized answers about video content while leveraging your own knowledge and memories - all without leaving the page.
## Development
1. Install dependencies:
```bash
npm install
```
2. Start development mode:
```bash
npm run watch
```
3. Build for production:
```bash
npm run build
```
## Features
- AI-powered chat interface directly in YouTube
- Memory capabilities powered by Mem0
- Dark mode support
- Customizable options
## Permissions
- activeTab: For accessing the current tab
- storage: For saving user preferences
- scripting: For injecting content scripts
## Host Permissions
- youtube.com
- openai.com
- mem0.ai
## Features
- **Contextual AI Chat**: Ask questions about videos you're watching
- **Seamless Integration**: Chat interface sits alongside YouTube's native UI
- **OpenAI-Powered**: Uses GPT models for intelligent responses
- **Customizable**: Configure model settings, appearance, and behavior
- **Future mem0 Integration**: Personalized responses based on your knowledge (coming soon)
## Installation
### From Source (Developer Mode)
1. Download or clone this repository
2. Open Chrome and navigate to `chrome://extensions/`
3. Enable "Developer mode" (toggle in the top-right corner)
4. Click "Load unpacked" and select the extension directory
5. The extension should now be installed and visible in your toolbar
### Setup
1. Click the extension icon in your toolbar
2. Enter your OpenAI API key (required to use the extension)
3. Configure additional settings if desired
4. Navigate to YouTube to start using the assistant
## Usage
1. Visit any YouTube video
2. Click the AI assistant icon in the corner of the page to open the chat interface
3. Ask questions about the video content
4. The AI will respond with contextual information
### Example Prompts
- "Can you summarize the main points of this video?"
- "What is the speaker explaining at 5:23?"
- "Explain the concept they just mentioned"
- "How does this relate to [topic I'm learning about]?"
- "What are some practical applications of what's being discussed?"
- **API Settings**: Change model, adjust tokens, modify temperature
- **Interface Settings**: Control where and how the chat appears
- **Behavior Settings**: Configure auto-context extraction
## Privacy & Data
- Your API keys are stored locally in your browser
- Video context and transcript is processed locally and only sent to OpenAI when you ask questions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,45 @@
{
"manifest_version": 3,
"name": "YouTube Assistant powered by Mem0",
"version": "1.0",
"description": "An AI-powered YouTube assistant with memory capabilities from Mem0",
"permissions": [
"activeTab",
"storage",
"scripting"
],
"host_permissions": [
"https://*.youtube.com/*",
"https://*.openai.com/*",
"https://*.mem0.ai/*"
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'",
"sandbox": "sandbox allow-scripts; script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self'"
},
"action": {
"default_popup": "public/popup.html"
},
"options_page": "public/options.html",
"content_scripts": [
{
"matches": ["https://*.youtube.com/*"],
"js": ["dist/content.bundle.js"],
"css": ["styles/content.css"]
}
],
"background": {
"service_worker": "src/background.js"
},
"web_accessible_resources": [
{
"resources": [
"assets/*",
"dist/*",
"styles/*",
"node_modules/mem0ai/dist/*"
],
"matches": ["https://*.youtube.com/*"]
}
]
}

View File

@@ -0,0 +1,26 @@
{
"name": "mem0-assistant",
"version": "1.0.0",
"description": "A Chrome extension that integrates AI chat functionality directly into YouTube and other sites. Get instant answers about video content without leaving the page.",
"main": "background.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"watch": "webpack --config webpack.config.js --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.22.0",
"@babel/preset-env": "^7.22.0",
"babel-loader": "^9.1.2",
"css-loader": "^7.1.2",
"style-loader": "^4.0.0",
"webpack": "^5.85.0",
"webpack-cli": "^5.1.1",
"youtube-transcript": "^1.0.6"
},
"dependencies": {
"mem0ai": "^2.1.15"
}
}

View File

@@ -0,0 +1,196 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>YouTube Assistant powered by Mem0</title>
<link rel="stylesheet" href="../styles/options.css">
</head>
<body>
<div class="main-content">
<header>
<div class="title-container">
<h1>YouTube Assistant</h1>
<div class="branding-container">
<span class="powered-by">powered by</span>
<a href="https://mem0.ai" target="_blank">
<img src="../assets/dark.svg" alt="Mem0 Logo" class="logo-img">
</a>
</div>
</div>
<div class="description">
Configure your YouTube Assistant preferences.
</div>
</header>
<div id="status-container"></div>
<div class="section">
<h2>Model Settings</h2>
<div class="form-group">
<label for="model">OpenAI Model</label>
<select id="model">
<option value="o3">o3</option>
<option value="o1">o1</option>
<option value="o1-mini">o1-mini</option>
<option value="o1-pro">o1-pro</option>
<option value="gpt-4o">GPT-4o</option>
<option value="gpt-4o-mini">GPT-4o mini</option>
</select>
<div class="description" style="margin-top: 8px; font-size: 13px">
Choose the OpenAI model to use depending on your needs.
</div>
</div>
<div class="form-group">
<label for="max-tokens">Maximum Response Length</label>
<input
type="number"
id="max-tokens"
min="50"
max="4000"
value="2000"
/>
<div class="description" style="margin-top: 8px; font-size: 13px">
Maximum number of tokens in the AI's response. Higher values allow
for longer responses but may increase processing time.
</div>
</div>
<div class="form-group">
<label for="temperature">Response Creativity</label>
<input
type="range"
id="temperature"
min="0"
max="1"
step="0.1"
value="0.7"
/>
<div
id="temperature-value"
style="display: inline-block; margin-left: 10px"
>
0.7
</div>
<div class="description" style="margin-top: 8px; font-size: 13px">
Controls response randomness. Lower values (0.1-0.3) are more
focused and deterministic, higher values (0.7-0.9) are more creative
and diverse.
</div>
</div>
</div>
<div class="section">
<h2>Create Memories</h2>
<div class="description">
Add information about yourself that you want the AI to remember. This
information will be used to provide more personalized responses.
</div>
<div class="form-group">
<label for="memory-input">Your Information</label>
<textarea
id="memory-input"
class="memory-input"
placeholder="Enter information about yourself that you want the AI to remember..."
></textarea>
</div>
<div class="actions">
<button id="add-memory" class="primary">
<span class="button-text">Add Memory</span>
</button>
</div>
<div id="memory-result" class="memory-result"></div>
</div>
<div class="actions">
<button id="reset-defaults" class="secondary-button">
Reset to Defaults
</button>
<button id="save-options">Save Changes</button>
</div>
</div>
<!-- Memories Sidebar -->
<div class="memories-sidebar" id="memories-sidebar">
<div class="memories-header">
<h2 class="memories-title">Your Memories</h2>
<div class="memories-actions">
<button
id="refresh-memories"
class="memory-action-btn"
title="Refresh Memories"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M23 4v6h-6"></path>
<path d="M1 20v-6h6"></path>
<path
d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"
></path>
</svg>
</button>
<button
id="delete-all-memories"
class="memory-action-btn delete"
title="Delete All Memories"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
</svg>
</button>
</div>
</div>
<div class="memories-list" id="memories-list">
<!-- Memories will be populated here -->
</div>
</div>
<!-- Edit Memory Modal -->
<div class="edit-memory-modal" id="edit-memory-modal">
<div class="edit-memory-content">
<div class="edit-memory-header">
<h3 class="edit-memory-title">Edit Memory</h3>
<button class="edit-memory-close" id="close-edit-modal">
&times;
</button>
</div>
<textarea class="edit-memory-textarea" id="edit-memory-text"></textarea>
<div class="edit-memory-actions">
<button class="memory-action-btn delete" id="delete-memory">
Delete
</button>
<button class="memory-action-btn" id="save-memory">
Save Changes
</button>
</div>
</div>
</div>
<script src="../dist/options.bundle.js"></script>
</body>
</html>

View File

@@ -0,0 +1,165 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>YouTube Assistant powered by Mem0</title>
<link rel="stylesheet" href="../styles/popup.css">
</head>
<body>
<header>
<h1>YouTube Assistant</h1>
<div class="branding-container">
<span class="powered-by">powered by</span>
<a href="https://mem0.ai" target="_blank">
<img src="../assets/dark.svg" alt="Mem0 Logo" class="logo-img">
</a>
</div>
</header>
<div class="content">
<!-- Status area -->
<div id="status-container"></div>
<!-- API key input, only shown if not set -->
<div id="api-key-section" class="api-key-section">
<label for="api-key">OpenAI API Key</label>
<div class="api-key-input-wrapper">
<input type="password" id="api-key" placeholder="sk-..." />
<button class="toggle-password" id="toggle-openai-key">
<svg
class="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
<button id="save-api-key" class="save-button">
<svg
class="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"
></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
Save OpenAI Key
</button>
</div>
<!-- mem0 API key input -->
<div id="mem0-api-key-section" class="api-key-section">
<label for="mem0-api-key">Mem0 API Key</label>
<div class="api-key-input-wrapper">
<input
type="password"
id="mem0-api-key"
placeholder="Enter your mem0 API key"
/>
<button class="toggle-password" id="toggle-mem0-key">
<svg
class="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
<div class="api-key-actions">
<p>Get your API key from <a href="https://mem0.ai" target="_blank" class="get-key-link">mem0.ai</a> to integrate memory features in the chat.</p>
<button id="save-mem0-api-key" class="save-button">
<svg
class="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"
></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
Save Mem0 Key
</button>
</div>
</div>
<!-- Action buttons -->
<div class="actions">
<button id="toggle-chat">
<svg
class="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
></path>
</svg>
Chat
</button>
<button id="open-options">
<svg
class="icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="3"></circle>
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
></path>
</svg>
Settings
</button>
</div>
<!-- Future mem0 integration status -->
<div class="mem0-status">
<p>
Mem0 integration:
<span id="mem0-status-text">Not configured</span>
</p>
</div>
</div>
<script src="../src/popup.js"></script>
</body>
</html>

View File

@@ -0,0 +1,255 @@
// Background script to handle API calls to OpenAI and manage extension state
// Configuration (will be stored in sync storage eventually)
let config = {
apiKey: "", // Will be set by user in options
mem0ApiKey: "", // Will be set by user in options
model: "gpt-4",
maxTokens: 2000,
temperature: 0.7,
enabledSites: ["youtube.com"],
};
// Track if config is loaded
let isConfigLoaded = false;
// Initialize configuration from storage
chrome.storage.sync.get(
["apiKey", "mem0ApiKey", "model", "maxTokens", "temperature", "enabledSites"],
(result) => {
if (result.apiKey) config.apiKey = result.apiKey;
if (result.mem0ApiKey) config.mem0ApiKey = result.mem0ApiKey;
if (result.model) config.model = result.model;
if (result.maxTokens) config.maxTokens = result.maxTokens;
if (result.temperature) config.temperature = result.temperature;
if (result.enabledSites) config.enabledSites = result.enabledSites;
isConfigLoaded = true;
}
);
// Listen for messages from content script or popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
// Handle different message types
switch (request.action) {
case "sendChatRequest":
sendChatRequest(request.messages, request.model || config.model)
.then((response) => sendResponse(response))
.catch((error) => sendResponse({ error: error.message }));
return true; // Required for async response
case "saveConfig":
saveConfig(request.config)
.then(() => sendResponse({ success: true }))
.catch((error) => sendResponse({ error: error.message }));
return true;
case "getConfig":
// If config isn't loaded yet, load it first
if (!isConfigLoaded) {
chrome.storage.sync.get(
[
"apiKey",
"mem0ApiKey",
"model",
"maxTokens",
"temperature",
"enabledSites",
],
(result) => {
if (result.apiKey) config.apiKey = result.apiKey;
if (result.mem0ApiKey) config.mem0ApiKey = result.mem0ApiKey;
if (result.model) config.model = result.model;
if (result.maxTokens) config.maxTokens = result.maxTokens;
if (result.temperature) config.temperature = result.temperature;
if (result.enabledSites) config.enabledSites = result.enabledSites;
isConfigLoaded = true;
sendResponse({ config });
}
);
return true;
}
sendResponse({ config });
return false;
case "openOptions":
// Open options page
chrome.runtime.openOptionsPage(() => {
if (chrome.runtime.lastError) {
console.error(
"Error opening options page:",
chrome.runtime.lastError
);
// Fallback: Try to open directly in a new tab
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") });
}
sendResponse({ success: true });
});
return true;
case "toggleChat":
// Forward the toggle request to the active tab
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
chrome.tabs
.sendMessage(tabs[0].id, { action: "toggleChat" })
.then((response) => sendResponse(response))
.catch((error) => sendResponse({ error: error.message }));
} else {
sendResponse({ error: "No active tab found" });
}
});
return true;
}
});
// Handle extension icon click - toggle chat visibility
chrome.action.onClicked.addListener((tab) => {
chrome.tabs
.sendMessage(tab.id, { action: "toggleChat" })
.catch((error) => console.error("Error toggling chat:", error));
});
// Save configuration to sync storage
async function saveConfig(newConfig) {
// Validate API key if provided
if (newConfig.apiKey) {
try {
const isValid = await validateApiKey(newConfig.apiKey);
if (!isValid) {
throw new Error("Invalid API key");
}
} catch (error) {
throw new Error(`API key validation failed: ${error.message}`);
}
}
// Update local config
config = { ...config, ...newConfig };
// Save to sync storage
return chrome.storage.sync.set(newConfig);
}
// Validate OpenAI API key with a simple request
async function validateApiKey(apiKey) {
try {
const response = await fetch("https://api.openai.com/v1/models", {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`API returned ${response.status}`);
}
return true;
} catch (error) {
console.error("API key validation error:", error);
return false;
}
}
// Send a chat request to OpenAI API
async function sendChatRequest(messages, model) {
// Check if API key is set
if (!config.apiKey) {
return {
error:
"API key not configured. Please set your OpenAI API key in the extension options.",
};
}
try {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${config.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: model || config.model,
messages: messages.map((msg) => ({
role: msg.role,
content: msg.content,
})),
max_tokens: config.maxTokens,
temperature: config.temperature,
stream: true, // Enable streaming
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
errorData.error?.message || `API returned ${response.status}`
);
}
// Create a ReadableStream from the response
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
// Process the stream
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Decode the chunk and add to buffer
buffer += decoder.decode(value, { stream: true });
// Process complete lines
const lines = buffer.split("\n");
buffer = lines.pop() || ""; // Keep the last incomplete line in the buffer
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") {
// Stream complete
return { done: true };
}
try {
const parsed = JSON.parse(data);
if (parsed.choices[0].delta.content) {
// Send the chunk to the content script
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
chrome.tabs.sendMessage(tabs[0].id, {
action: "streamChunk",
chunk: parsed.choices[0].delta.content,
});
}
});
}
} catch (e) {
console.error("Error parsing chunk:", e);
}
}
}
}
return { done: true };
} catch (error) {
console.error("Error sending chat request:", error);
return { error: error.message };
}
}
// Future: Add mem0 integration functions here
// When ready, replace with actual implementation
function mem0Integration() {
// Placeholder for future mem0 integration
return {
getUserMemories: async (userId) => {
return { memories: [] };
},
saveMemory: async (userId, memory) => {
return { success: true };
},
};
}

View File

@@ -0,0 +1,657 @@
// 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(/&amp;#39;/g, "'")
.replace(/&amp;quot;/g, '"')
.replace(/&amp;lt;/g, "<")
.replace(/&amp;gt;/g, ">")
.replace(/&amp;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 = `
<div class="ai-chat-header">
<div class="ai-chat-tabs">
<button class="ai-chat-tab active" data-tab="chat">Chat</button>
<button class="ai-chat-tab" data-tab="memories">Memories</button>
</div>
<div class="ai-chat-controls">
<button id="ai-chat-minimize" class="ai-chat-btn" title="Minimize">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
<button id="ai-chat-close" class="ai-chat-btn" title="Close">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<div class="ai-chat-body">
<div id="ai-chat-content" class="ai-chat-content">
<div id="ai-chat-messages" class="ai-chat-messages"></div>
<div class="ai-chat-input-container">
<textarea id="ai-chat-input" placeholder="Ask about this video..."></textarea>
<button id="ai-chat-send" class="ai-chat-send-btn" title="Send message">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
</div>
<div id="ai-chat-memories" class="ai-chat-memories" style="display: none;">
<div class="memories-header">
<div class="memories-title">
Manage memories <a href="#" id="manage-memories-link" title="Open options page">here <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg></a>
</div>
<button id="refresh-memories" class="ai-chat-btn" title="Refresh memories">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M23 4v6h-6"></path>
<path d="M1 20v-6h6"></path>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
</button>
</div>
<div id="memories-list" class="memories-list"></div>
</div>
</div>
`;
// 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, "<pre><code>$1</code></pre>")
// Inline code
.replace(/`([^`]+)`/g, "<code>$1</code>")
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
// Bold text
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
// Italic text
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
// Lists
.replace(/^\s*[-*]\s+(.+)$/gm, "<li>$1</li>")
.replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>")
// Line breaks
.replace(/\n/g, "<br>");
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, "<pre><code>$1</code></pre>")
// Inline code
.replace(/`([^`]+)`/g, "<code>$1</code>")
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
// Bold text
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
// Italic text
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
// Lists
.replace(/^\s*[-*]\s+(.+)$/gm, "<li>$1</li>")
.replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>")
// Line breaks
.replace(/\n/g, "<br>");
}
// 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 =
'<div class="loading">Loading memories...</div>';
// If client isn't initialized, try to initialize it
if (!mem0client) {
const initialized = await initializeMem0AI();
if (!initialized) {
memoriesContainer.innerHTML =
'<div class="error">Please set your Mem0 API key in the extension options.</div>';
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 =
'<div class="no-memories">No memories found</div>';
}
} else {
memoriesContainer.innerHTML =
'<div class="no-memories">No memories found</div>';
}
} catch (error) {
console.error("Error loading memories:", error);
document.getElementById("memories-list").innerHTML =
'<div class="error">Error loading memories. Please try again.</div>';
}
}

View File

@@ -0,0 +1,452 @@
// Options page functionality for AI Chat Assistant
import { MemoryClient } from "mem0ai";
// Default configuration
const defaultConfig = {
model: "gpt-4o",
maxTokens: 2000,
temperature: 0.7,
enabledSites: ["youtube.com"],
};
// Initialize Mem0AI client
let mem0client = null;
// Initialize when the DOM is fully loaded
document.addEventListener("DOMContentLoaded", init);
// Initialize options page
async function init() {
// Set up event listeners
document
.getElementById("save-options")
.addEventListener("click", saveOptions);
document
.getElementById("reset-defaults")
.addEventListener("click", resetToDefaults);
document.getElementById("add-memory").addEventListener("click", addMemory);
// Set up slider value display
const temperatureSlider = document.getElementById("temperature");
const temperatureValue = document.getElementById("temperature-value");
temperatureSlider.addEventListener("input", () => {
temperatureValue.textContent = temperatureSlider.value;
});
// Set up memories sidebar functionality
document
.getElementById("refresh-memories")
.addEventListener("click", fetchMemories);
document
.getElementById("delete-all-memories")
.addEventListener("click", deleteAllMemories);
document
.getElementById("close-edit-modal")
.addEventListener("click", closeEditModal);
document.getElementById("save-memory").addEventListener("click", saveMemory);
document
.getElementById("delete-memory")
.addEventListener("click", deleteMemory);
// Load current configuration
await loadConfig();
// Initialize Mem0AI and load memories
await initializeMem0AI();
await fetchMemories();
}
// Initialize Mem0AI with API key from storage
async function initializeMem0AI() {
try {
const response = await chrome.runtime.sendMessage({ action: "getConfig" });
const mem0ApiKey = response.config.mem0ApiKey;
if (!mem0ApiKey) {
showMemoriesError("Please configure your Mem0 API key in the popup");
return false;
}
mem0client = new MemoryClient({
apiKey: mem0ApiKey,
projectId: "youtube-assistant",
isExtension: true,
});
return true;
} catch (error) {
console.error("Error initializing Mem0AI:", error);
showMemoriesError("Failed to initialize Mem0AI");
return false;
}
}
// Load configuration from storage
async function loadConfig() {
try {
const response = await chrome.runtime.sendMessage({ action: "getConfig" });
const config = response.config;
// Update form fields with current values
if (config.model) {
document.getElementById("model").value = config.model;
}
if (config.maxTokens) {
document.getElementById("max-tokens").value = config.maxTokens;
}
if (config.temperature !== undefined) {
const temperatureSlider = document.getElementById("temperature");
temperatureSlider.value = config.temperature;
document.getElementById("temperature-value").textContent =
config.temperature;
}
} catch (error) {
showStatus(`Error loading configuration: ${error.message}`, "error");
}
}
// Save options to storage
async function saveOptions() {
// Get values from form
const model = document.getElementById("model").value;
const maxTokens = parseInt(document.getElementById("max-tokens").value);
const temperature = parseFloat(document.getElementById("temperature").value);
// Validate inputs
if (maxTokens < 50 || maxTokens > 4000) {
showStatus("Maximum tokens must be between 50 and 4000", "error");
return;
}
if (temperature < 0 || temperature > 1) {
showStatus("Temperature must be between 0 and 1", "error");
return;
}
// Prepare config object
const config = {
model,
maxTokens,
temperature,
};
// Show loading status
showStatus("Saving options...", "warning");
try {
// Send to background script for saving
const response = await chrome.runtime.sendMessage({
action: "saveConfig",
config,
});
if (response.error) {
showStatus(`Error: ${response.error}`, "error");
} else {
showStatus("Options saved successfully", "success");
loadConfig(); // Refresh the UI with the latest saved values
}
} catch (error) {
showStatus(`Error: ${error.message}`, "error");
}
}
// Reset options to defaults
function resetToDefaults() {
if (
confirm(
"Are you sure you want to reset all options to their default values?"
)
) {
// Set form fields to default values
document.getElementById("model").value = defaultConfig.model;
document.getElementById("max-tokens").value = defaultConfig.maxTokens;
const temperatureSlider = document.getElementById("temperature");
temperatureSlider.value = defaultConfig.temperature;
document.getElementById("temperature-value").textContent =
defaultConfig.temperature;
showStatus("Restored default values. Click Save to apply.", "warning");
}
}
// Memories functionality
let currentMemory = null;
async function fetchMemories() {
try {
if (!mem0client) {
const initialized = await initializeMem0AI();
if (!initialized) return;
}
const memories = await mem0client.getAll({
user_id: "youtube-assistant-mem0",
page: 1,
page_size: 50,
});
displayMemories(memories.results);
} catch (error) {
console.error("Error fetching memories:", error);
showMemoriesError("Failed to load memories");
}
}
function displayMemories(memories) {
const memoriesList = document.getElementById("memories-list");
memoriesList.innerHTML = "";
if (memories.length === 0) {
memoriesList.innerHTML = `
<div class="memory-item">
<div class="memory-content">No memories found. Your memories will appear here.</div>
</div>
`;
return;
}
memories.forEach((memory) => {
const memoryElement = document.createElement("div");
memoryElement.className = "memory-item";
memoryElement.innerHTML = `
<div class="memory-content">${memory.memory}</div>
<div class="memory-meta">Last updated: ${new Date(
memory.updated_at
).toLocaleString()}</div>
<div class="memory-actions">
<button class="memory-action-btn edit" data-id="${
memory.id
}">Edit</button>
<button class="memory-action-btn delete" data-id="${
memory.id
}">Delete</button>
</div>
`;
// Add event listeners
memoryElement
.querySelector(".edit")
.addEventListener("click", () => editMemory(memory));
memoryElement
.querySelector(".delete")
.addEventListener("click", () => deleteMemory(memory.id));
memoriesList.appendChild(memoryElement);
});
}
function showMemoriesError(message) {
const memoriesList = document.getElementById("memories-list");
memoriesList.innerHTML = `
<div class="memory-item">
<div class="memory-content">${message}</div>
</div>
`;
}
async function deleteAllMemories() {
if (
!confirm(
"Are you sure you want to delete all memories? This action cannot be undone."
)
) {
return;
}
try {
if (!mem0client) {
const initialized = await initializeMem0AI();
if (!initialized) return;
}
await mem0client.deleteAll({
user_id: "youtube-assistant-mem0",
});
showStatus("All memories deleted successfully", "success");
await fetchMemories();
} catch (error) {
console.error("Error deleting memories:", error);
showStatus("Failed to delete memories", "error");
}
}
function editMemory(memory) {
currentMemory = memory;
const modal = document.getElementById("edit-memory-modal");
const textarea = document.getElementById("edit-memory-text");
textarea.value = memory.memory;
modal.classList.add("open");
}
function closeEditModal() {
const modal = document.getElementById("edit-memory-modal");
modal.classList.remove("open");
currentMemory = null;
}
async function saveMemory() {
if (!currentMemory) return;
try {
if (!mem0client) {
const initialized = await initializeMem0AI();
if (!initialized) return;
}
const textarea = document.getElementById("edit-memory-text");
const updatedMemory = textarea.value.trim();
if (!updatedMemory) {
showStatus("Memory cannot be empty", "error");
return;
}
await mem0client.update(currentMemory.id, updatedMemory);
showStatus("Memory updated successfully", "success");
closeEditModal();
await fetchMemories();
} catch (error) {
console.error("Error updating memory:", error);
showStatus("Failed to update memory", "error");
}
}
async function deleteMemory(memoryId) {
if (
!confirm(
"Are you sure you want to delete this memory? This action cannot be undone."
)
) {
return;
}
try {
if (!mem0client) {
const initialized = await initializeMem0AI();
if (!initialized) return;
}
await mem0client.delete(memoryId);
showStatus("Memory deleted successfully", "success");
await fetchMemories();
} catch (error) {
console.error("Error deleting memory:", error);
showStatus("Failed to delete memory", "error");
}
}
// Show status message
function showStatus(message, type = "info") {
const statusContainer = document.getElementById("status-container");
// Clear previous status
statusContainer.innerHTML = "";
// Create status element
const statusElement = document.createElement("div");
statusElement.className = `status ${type}`;
statusElement.textContent = message;
// Add to container
statusContainer.appendChild(statusElement);
// Auto-clear success messages after 3 seconds
if (type === "success") {
setTimeout(() => {
statusElement.style.opacity = "0";
setTimeout(() => {
if (statusContainer.contains(statusElement)) {
statusContainer.removeChild(statusElement);
}
}, 300);
}, 3000);
}
}
// Add memory to Mem0
async function addMemory() {
const memoryInput = document.getElementById("memory-input");
const addButton = document.getElementById("add-memory");
const memoryResult = document.getElementById("memory-result");
const buttonText = addButton.querySelector(".button-text");
const content = memoryInput.value.trim();
if (!content) {
showMemoryResult(
"Please enter some information to add as a memory",
"error"
);
return;
}
// Show loading state
addButton.disabled = true;
buttonText.textContent = "Adding...";
addButton.innerHTML =
'<div class="loading-spinner"></div><span class="button-text">Adding...</span>';
memoryResult.style.display = "none";
try {
if (!mem0client) {
const initialized = await initializeMem0AI();
if (!initialized) return;
}
const result = await mem0client.add(
[
{
role: "user",
content: content,
},
],
{
user_id: "youtube-assistant-mem0",
}
);
// Show success message with number of memories added
showMemoryResult(
`Added ${result.length || 0} new ${
result.length === 1 ? "memory" : "memories"
}`,
"success"
);
// Clear the input
memoryInput.value = "";
// Refresh the memories list
await fetchMemories();
} catch (error) {
showMemoryResult(`Error adding memory: ${error.message}`, "error");
} finally {
// Reset button state
addButton.disabled = false;
buttonText.textContent = "Add Memory";
addButton.innerHTML = '<span class="button-text">Add Memory</span>';
}
}
// Show memory result message
function showMemoryResult(message, type) {
const memoryResult = document.getElementById("memory-result");
memoryResult.textContent = message;
memoryResult.className = `memory-result ${type}`;
memoryResult.style.display = "block";
// Auto-clear success messages after 3 seconds
if (type === "success") {
setTimeout(() => {
memoryResult.style.opacity = "0";
setTimeout(() => {
memoryResult.style.display = "none";
memoryResult.style.opacity = "1";
}, 300);
}, 3000);
}
}

View File

@@ -0,0 +1,241 @@
// Popup functionality for AI Chat Assistant
document.addEventListener("DOMContentLoaded", init);
// Initialize popup
async function init() {
try {
// Set up event listeners
document
.getElementById("toggle-chat")
.addEventListener("click", toggleChat);
document
.getElementById("open-options")
.addEventListener("click", openOptions);
document
.getElementById("save-api-key")
.addEventListener("click", saveApiKey);
document
.getElementById("save-mem0-api-key")
.addEventListener("click", saveMem0ApiKey);
// Set up password toggle listeners
document
.getElementById("toggle-openai-key")
.addEventListener("click", () => togglePasswordVisibility("api-key"));
document
.getElementById("toggle-mem0-key")
.addEventListener("click", () =>
togglePasswordVisibility("mem0-api-key")
);
// Load current configuration and wait for it to complete
await loadConfig();
} catch (error) {
console.error("Initialization error:", error);
showStatus("Error initializing popup", "error");
}
}
// Toggle chat visibility in the active tab
function toggleChat() {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs[0]) {
// First check if we can inject the content script
chrome.scripting
.executeScript({
target: { tabId: tabs[0].id },
files: ["dist/content.bundle.js"],
})
.then(() => {
// Now try to toggle the chat
chrome.tabs
.sendMessage(tabs[0].id, { action: "toggleChat" })
.then((response) => {
if (response && response.error) {
console.error("Error toggling chat:", response.error);
showStatus(
"Chat interface not available on this page",
"warning"
);
} else {
// Close the popup after successful toggle
window.close();
}
})
.catch((error) => {
console.error("Error toggling chat:", error);
showStatus(
"Chat interface not available on this page",
"warning"
);
});
})
.catch((error) => {
console.error("Error injecting content script:", error);
showStatus("Cannot inject chat interface on this page", "error");
});
}
});
}
// Open options page
function openOptions() {
// Send message to background script to handle opening options
chrome.runtime.sendMessage({ action: "openOptions" }, (response) => {
if (chrome.runtime.lastError) {
console.error("Error opening options:", chrome.runtime.lastError);
// Direct fallback if communication with background script fails
try {
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") });
} catch (err) {
console.error("Fallback failed:", err);
// Last resort
window.open(chrome.runtime.getURL("options.html"), "_blank");
}
}
});
}
// Toggle password visibility
function togglePasswordVisibility(inputId) {
const input = document.getElementById(inputId);
const type = input.type === "password" ? "text" : "password";
input.type = type;
// Update the eye icon
const button = input.nextElementSibling;
const icon = button.querySelector(".icon");
if (type === "text") {
icon.innerHTML =
'<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>';
} else {
icon.innerHTML =
'<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>';
}
}
// Save API key to storage
async function saveApiKey() {
const apiKeyInput = document.getElementById("api-key");
const apiKey = apiKeyInput.value.trim();
// Show loading status
showStatus("Saving API key...", "warning");
try {
// Send to background script for validation and saving
const response = await chrome.runtime.sendMessage({
action: "saveConfig",
config: { apiKey },
});
if (response.error) {
showStatus(`Error: ${response.error}`, "error");
} else {
showStatus("API key saved successfully", "success");
loadConfig(); // Refresh the UI
}
} catch (error) {
showStatus(`Error: ${error.message}`, "error");
}
}
// Save mem0 API key to storage
async function saveMem0ApiKey() {
const apiKeyInput = document.getElementById("mem0-api-key");
const apiKey = apiKeyInput.value.trim();
// Show loading status
showStatus("Saving Mem0 API key...", "warning");
try {
// Send to background script for saving
const response = await chrome.runtime.sendMessage({
action: "saveConfig",
config: { mem0ApiKey: apiKey },
});
if (response.error) {
showStatus(`Error: ${response.error}`, "error");
} else {
showStatus("Mem0 API key saved successfully", "success");
loadConfig(); // Refresh the UI
}
} catch (error) {
showStatus(`Error: ${error.message}`, "error");
}
}
// Load configuration from storage
async function loadConfig() {
try {
// Add a small delay to ensure background script is ready
await new Promise((resolve) => setTimeout(resolve, 100));
const response = await chrome.runtime.sendMessage({ action: "getConfig" });
const config = response.config || {};
// Update OpenAI API key field
const apiKeyInput = document.getElementById("api-key");
if (config.apiKey) {
apiKeyInput.value = config.apiKey;
apiKeyInput.type = "password"; // Ensure it's hidden by default
document.getElementById("api-key-section").style.display = "block";
} else {
apiKeyInput.value = "";
document.getElementById("api-key-section").style.display = "block";
showStatus("Please set your OpenAI API key", "warning");
}
// Update mem0 API key field
const mem0ApiKeyInput = document.getElementById("mem0-api-key");
if (config.mem0ApiKey) {
mem0ApiKeyInput.value = config.mem0ApiKey;
mem0ApiKeyInput.type = "password"; // Ensure it's hidden by default
document.getElementById("mem0-api-key-section").style.display = "block";
document.getElementById("mem0-status-text").textContent = "Connected";
document.getElementById("mem0-status-text").style.color =
"var(--success-color)";
} else {
mem0ApiKeyInput.value = "";
document.getElementById("mem0-api-key-section").style.display = "block";
document.getElementById("mem0-status-text").textContent =
"Not configured";
document.getElementById("mem0-status-text").style.color =
"var(--warning-color)";
}
} catch (error) {
console.error("Error loading configuration:", error);
showStatus(`Error loading configuration: ${error.message}`, "error");
}
}
// Show status message
function showStatus(message, type = "info") {
const statusContainer = document.getElementById("status-container");
// Clear previous status
statusContainer.innerHTML = "";
// Create status element
const statusElement = document.createElement("div");
statusElement.className = `status ${type}`;
statusElement.textContent = message;
// Add to container
statusContainer.appendChild(statusElement);
// Auto-clear success messages after 3 seconds
if (type === "success") {
setTimeout(() => {
statusElement.style.opacity = "0";
setTimeout(() => {
if (statusContainer.contains(statusElement)) {
statusContainer.removeChild(statusElement);
}
}, 300);
}, 3000);
}
}

View File

@@ -0,0 +1,492 @@
/* Styles for the AI Chat Assistant */
/* Modern Dark Theme with Blue Accents */
:root {
--chat-dark-bg: #1a1a1a;
--chat-darker-bg: #121212;
--chat-light-text: #f1f1f1;
--chat-blue-accent: #3d84f7;
--chat-blue-hover: #2d74e7;
--chat-blue-light: rgba(61, 132, 247, 0.15);
--chat-error: #ff4a4a;
--chat-border-radius: 12px;
--chat-message-radius: 12px;
--chat-transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Main container */
#ai-chat-assistant-container {
position: fixed;
right: 20px;
bottom: 20px;
width: 380px;
height: 550px;
background-color: var(--chat-dark-bg);
border-radius: var(--chat-border-radius);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
z-index: 9999;
overflow: hidden;
transition: var(--chat-transition);
opacity: 0;
transform: translateY(20px) scale(0.98);
pointer-events: none;
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif;
border: 1px solid rgba(255, 255, 255, 0.08);
}
/* When visible */
#ai-chat-assistant-container.visible {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: all;
}
/* When minimized */
#ai-chat-assistant-container.minimized {
height: 50px;
}
#ai-chat-assistant-container.minimized .ai-chat-body {
display: none;
}
/* Header */
.ai-chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: var(--chat-darker-bg);
color: var(--chat-light-text);
border-top-left-radius: var(--chat-border-radius);
border-top-right-radius: var(--chat-border-radius);
cursor: move;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.ai-chat-title {
font-weight: 500;
font-size: 15px;
display: flex;
align-items: center;
gap: 6px;
}
.ai-chat-title::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
background-color: var(--chat-blue-accent);
border-radius: 50%;
box-shadow: 0 0 10px var(--chat-blue-accent);
}
.ai-chat-controls {
display: flex;
gap: 8px;
}
.ai-chat-btn {
background: none;
border: none;
color: var(--chat-light-text);
font-size: 18px;
cursor: pointer;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: var(--chat-transition);
}
.ai-chat-btn:hover {
background-color: rgba(255, 255, 255, 0.08);
}
/* Body */
.ai-chat-body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: var(--chat-dark-bg);
}
/* Messages container */
.ai-chat-messages {
flex: 1;
overflow-y: auto;
padding: 15px;
display: flex;
flex-direction: column;
gap: 12px;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
.ai-chat-messages::-webkit-scrollbar {
width: 5px;
}
.ai-chat-messages::-webkit-scrollbar-track {
background: transparent;
}
.ai-chat-messages::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
/* Individual message */
.ai-chat-message {
max-width: 85%;
padding: 12px 16px;
border-radius: var(--chat-message-radius);
line-height: 1.5;
position: relative;
font-size: 14px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
animation: message-fade-in 0.3s ease;
word-break: break-word;
}
@keyframes message-fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* User message */
.ai-chat-message.user {
align-self: flex-end;
background-color: var(--chat-blue-accent);
color: white;
border-bottom-right-radius: 4px;
}
/* Assistant message */
.ai-chat-message.assistant {
align-self: flex-start;
background-color: rgba(255, 255, 255, 0.08);
color: var(--chat-light-text);
border-bottom-left-radius: 4px;
}
/* System message */
.ai-chat-message.system {
align-self: center;
background-color: rgba(255, 76, 76, 0.1);
color: var(--chat-error);
max-width: 90%;
font-size: 13px;
border-radius: 8px;
border: 1px solid rgba(255, 76, 76, 0.2);
}
/* Loading animation */
.ai-chat-message.loading {
background-color: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.7);
}
.ai-chat-message.loading:after {
content: "...";
animation: thinking 1.5s infinite;
}
@keyframes thinking {
0% { content: "."; }
33% { content: ".."; }
66% { content: "..."; }
}
/* Input area */
.ai-chat-input-container {
display: flex;
padding: 12px 16px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
background-color: var(--chat-darker-bg);
}
#ai-chat-input {
flex: 1;
border: 1px solid rgba(255, 255, 255, 0.1);
background-color: rgba(255, 255, 255, 0.05);
color: var(--chat-light-text);
border-radius: 20px;
padding: 10px 16px;
font-size: 14px;
resize: none;
max-height: 100px;
outline: none;
font-family: inherit;
transition: var(--chat-transition);
}
#ai-chat-input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
#ai-chat-input:focus {
border-color: var(--chat-blue-accent);
background-color: rgba(255, 255, 255, 0.07);
box-shadow: 0 0 0 1px rgba(61, 132, 247, 0.1);
}
.ai-chat-send-btn {
background: none;
border: none;
color: var(--chat-blue-accent);
cursor: pointer;
padding: 8px;
margin-left: 8px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: var(--chat-transition);
}
.ai-chat-send-btn:hover {
background-color: var(--chat-blue-light);
transform: scale(1.05);
}
/* Toggle button */
.ai-chat-toggle {
position: fixed;
right: 20px;
bottom: 20px;
width: 56px;
height: 56px;
border-radius: 50%;
background-color: var(--chat-blue-accent);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 15px rgba(61, 132, 247, 0.35);
z-index: 9998;
transition: var(--chat-transition);
border: none;
}
.ai-chat-toggle:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(61, 132, 247, 0.45);
}
#ai-chat-assistant-container.visible + .ai-chat-toggle {
transform: scale(0);
opacity: 0;
}
/* Code formatting */
.ai-chat-message pre {
background-color: rgba(0, 0, 0, 0.3);
padding: 10px;
border-radius: 6px;
overflow-x: auto;
margin: 10px 0;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.ai-chat-message code {
font-family: 'Cascadia Code', 'Fira Code', 'Source Code Pro', monospace;
font-size: 12px;
}
.ai-chat-message.user code {
background-color: rgba(255, 255, 255, 0.2);
padding: 2px 5px;
border-radius: 3px;
}
.ai-chat-message.assistant code {
background-color: rgba(0, 0, 0, 0.3);
padding: 2px 5px;
border-radius: 3px;
color: #e2e2e2;
}
/* Links */
.ai-chat-message a {
color: var(--chat-blue-accent);
text-decoration: none;
border-bottom: 1px dotted rgba(61, 132, 247, 0.5);
transition: var(--chat-transition);
}
.ai-chat-message a:hover {
border-bottom: 1px solid var(--chat-blue-accent);
}
.ai-chat-message.user a {
color: white;
border-bottom: 1px dotted rgba(255, 255, 255, 0.5);
}
.ai-chat-message.user a:hover {
border-bottom: 1px solid white;
}
/* Responsive adjustments */
@media (max-width: 768px) {
#ai-chat-assistant-container {
width: calc(100% - 20px);
height: 60vh;
right: 10px;
bottom: 10px;
}
.ai-chat-toggle {
right: 10px;
bottom: 10px;
}
}
/* Tab styles */
.ai-chat-tabs {
display: flex;
gap: 10px;
margin-right: 10px;
}
.ai-chat-tab {
background: none;
border: none;
color: var(--chat-light-text);
padding: 5px 10px;
cursor: pointer;
font-size: 14px;
border-radius: 4px;
transition: var(--chat-transition);
}
.ai-chat-tab:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.ai-chat-tab.active {
background-color: var(--chat-blue-accent);
color: white;
}
/* Content area */
.ai-chat-content {
display: flex;
flex-direction: column;
height: 100%;
}
/* Memories tab styles */
.ai-chat-memories {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--chat-dark-bg);
}
.memories-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
padding-left: 16px;
padding-right: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.memories-title {
display: inline;
align-items: center;
font-size: 14px;
color: var(--chat-light-text);
}
.memories-title a {
color: var(--chat-blue-accent);
text-decoration: none;
font-weight: 500;
transition: var(--chat-transition);
display: inline-flex;
align-items: center;
gap: 4px;
}
.memories-title a:hover {
color: var(--chat-blue-hover);
text-decoration: underline;
}
.memories-title a svg {
vertical-align: middle;
}
.memories-title svg {
vertical-align: middle;
margin-left: 4px;
}
.memories-list {
flex: 1;
overflow-y: auto;
padding: 10px;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
.memories-list::-webkit-scrollbar {
width: 5px;
}
.memories-list::-webkit-scrollbar-track {
background: transparent;
}
.memories-list::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.memory-item {
background-color: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: var(--chat-message-radius);
padding: 12px 16px;
margin-bottom: 10px;
font-size: 14px;
line-height: 1.4;
color: var(--chat-light-text);
}
.memory-item:last-child {
margin-bottom: 0;
}
.loading, .no-memories, .error, .info {
text-align: center;
padding: 20px;
font-size: 14px;
color: var(--chat-light-text);
}
.error {
color: var(--chat-error);
font-size: 14px;
}
.info {
color: var(--chat-blue-accent);
}

View File

@@ -0,0 +1,587 @@
:root {
--dark-bg: #1a1a1a;
--darker-bg: #121212;
--section-bg: #202020;
--light-text: #f1f1f1;
--dim-text: rgba(255, 255, 255, 0.7);
--dim-text-2: rgba(255, 255, 255, 0.5);
--blue-accent: #3d84f7;
--blue-hover: #2d74e7;
--blue-light: rgba(61, 132, 247, 0.15);
--error-color: #ff4a4a;
--warning-color: #ffaa33;
--success-color: #4caf50;
--border-radius: 8px;
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
font-family: "Roboto", -apple-system, BlinkMacSystemFont, sans-serif;
margin: 0;
padding: 20px 20px 40px;
color: var(--light-text);
background-color: var(--dark-bg);
max-width: 1200px;
margin: 0 auto;
}
header {
max-width: 800px;
padding-left: 28px;
padding-top: 10px;
color: #f1f1f1;
}
h1 {
font-size: 32px;
margin: 0 0 12px 0;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
.title-container {
display: flex;
align-items: center;
gap: 10px;
}
.logo-img {
height: 20px;
width: auto;
margin-left: 8px;
position: relative;
top: 1px;
}
.powered-by {
font-size: 12px;
font-weight: normal;
color: rgba(255, 255, 255, 0.6);
line-height: 1;
}
.branding-container {
display: flex;
align-items: center;
justify-content: center;
}
.description {
color: var(--dim-text);
margin-bottom: 20px;
font-size: 15px;
line-height: 1.5;
}
.section {
margin-bottom: 30px;
background: var(--section-bg);
padding: 28px;
border-radius: var(--border-radius);
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
h2 {
font-size: 18px;
margin-top: 0;
margin-bottom: 15px;
color: var(--light-text);
display: flex;
align-items: center;
gap: 8px;
}
h2::before {
content: "";
display: inline-block;
width: 5px;
height: 20px;
background-color: var(--blue-accent);
border-radius: 3px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--light-text);
}
input[type="text"],
input[type="password"],
input[type="number"],
select {
width: 100%;
padding: 12px;
background-color: rgba(255, 255, 255, 0.05);
color: var(--light-text);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--border-radius);
font-size: 14px;
box-sizing: border-box;
transition: var(--transition);
}
input[type="text"]:focus,
input[type="password"]:focus,
input[type="number"]:focus,
select:focus {
border-color: var(--blue-accent);
outline: none;
box-shadow: 0 0 0 1px rgba(61, 132, 247, 0.2);
}
select {
appearance: none;
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%207l5%205%205-5%22%20stroke%3D%22%23fff%22%20stroke-width%3D%221.5%22%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
}
input[type="number"] {
width: 120px;
}
input[type="checkbox"] {
margin-right: 10px;
position: relative;
width: 18px;
height: 18px;
-webkit-appearance: none;
appearance: none;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
cursor: pointer;
transition: var(--transition);
}
input[type="checkbox"]:checked {
background-color: var(--blue-accent);
border-color: var(--blue-accent);
}
input[type="checkbox"]:checked::after {
content: "";
position: absolute;
left: 5px;
top: 2px;
width: 6px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
input[type="checkbox"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.checkbox-label {
display: flex;
align-items: center;
margin-bottom: 12px;
font-size: 14px;
color: var(--light-text);
}
.checkbox-label label {
margin-bottom: 0;
margin-left: 8px;
}
button {
background-color: var(--blue-accent);
color: white;
border: none;
padding: 12px 20px;
border-radius: var(--border-radius);
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
button:hover {
background-color: var(--blue-hover);
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
button:active {
transform: translateY(1px);
box-shadow: none;
}
button:disabled {
background-color: rgba(255, 255, 255, 0.1);
color: var(--dim-text-2);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.status {
padding: 15px;
border-radius: var(--border-radius);
margin-top: 20px;
font-size: 14px;
animation: fade-in 0.3s ease;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.status.error {
background-color: rgba(255, 74, 74, 0.1);
color: var(--error-color);
border: 1px solid rgba(255, 74, 74, 0.2);
}
.status.success {
background-color: rgba(76, 175, 80, 0.1);
color: var(--success-color);
border: 1px solid rgba(76, 175, 80, 0.2);
}
.status.warning {
background-color: rgba(255, 170, 51, 0.1);
color: var(--warning-color);
border: 1px solid rgba(255, 170, 51, 0.2);
}
.actions {
display: flex;
gap: 10px;
}
.secondary-button {
background-color: rgba(255, 255, 255, 0.08);
color: var(--light-text);
}
.secondary-button:hover {
background-color: rgba(255, 255, 255, 0.12);
}
.api-key-container {
display: flex;
gap: 10px;
}
.api-key-container input {
flex: 1;
}
/* Slider styles */
.slider-container {
margin-top: 12px;
display: flex;
align-items: center;
}
.slider {
-webkit-appearance: none;
flex: 1;
height: 4px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--blue-accent);
cursor: pointer;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
transition: var(--transition);
}
.slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.4);
}
.slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--blue-accent);
cursor: pointer;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
transition: var(--transition);
border: none;
}
.slider::-moz-range-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.4);
}
/* Add styles for memory creation section */
.memory-input {
width: 100%;
min-height: 150px;
padding: 12px;
background-color: rgba(255, 255, 255, 0.05);
color: var(--light-text);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--border-radius);
font-size: 14px;
box-sizing: border-box;
transition: var(--transition);
resize: vertical;
font-family: inherit;
}
.memory-input:focus {
border-color: var(--blue-accent);
outline: none;
box-shadow: 0 0 0 1px rgba(61, 132, 247, 0.2);
}
.memory-result {
margin-top: 15px;
padding: 12px;
border-radius: var(--border-radius);
font-size: 14px;
display: none;
}
.memory-result.success {
background-color: rgba(76, 175, 80, 0.1);
color: var(--success-color);
border: 1px solid rgba(76, 175, 80, 0.2);
display: block;
}
.memory-result.error {
background-color: rgba(255, 74, 74, 0.1);
color: var(--error-color);
border: 1px solid rgba(255, 74, 74, 0.2);
display: block;
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--light-text);
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Add new styles for the memories sidebar */
.memories-sidebar {
position: fixed;
top: 0;
right: 0;
width: 384px;
height: 100vh;
background: var(--section-bg);
border-left: 1px solid rgba(255, 255, 255, 0.05);
transition: transform 0.3s ease;
z-index: 1000;
display: flex;
flex-direction: column;
}
.memories-sidebar.collapsed {
transform: translateX(384px);
}
.memories-header {
padding: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
}
.memories-title {
font-size: 16px;
font-weight: 500;
color: var(--light-text);
}
.memories-actions {
display: flex;
gap: 8px;
}
.memories-list {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.memory-item {
padding: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: var(--border-radius);
margin-bottom: 12px;
cursor: pointer;
transition: var(--transition);
}
.memory-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.memory-content {
font-size: 14px;
color: var(--light-text);
margin-bottom: 8px;
text-align: center;
text-wrap-style: pretty;
}
.memory-item .memory-content {
text-align: left;
}
.memory-meta {
font-size: 12px;
color: var(--dim-text);
}
.memory-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.memory-action-btn {
padding: 8px;
font-size: 12px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
color: var(--light-text);
border: none;
cursor: pointer;
transition: var(--transition);
}
.memory-action-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.memory-action-btn.delete:hover {
background-color: var(--error-color);
}
.edit-memory-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1100;
align-items: center;
justify-content: center;
}
.edit-memory-modal.open {
display: flex;
}
.edit-memory-content {
display: flex;
flex-direction: column;
background: var(--section-bg);
padding: 24px;
border-radius: var(--border-radius);
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.edit-memory-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.edit-memory-title {
font-size: 18px;
font-weight: 500;
color: var(--light-text);
}
.edit-memory-close {
background: none;
border: none;
color: var(--dim-text);
cursor: pointer;
padding: 4px;
font-size: 20px;
width: 30px;
}
.edit-memory-textarea {
min-height: 20px;
max-height: 70px;
padding: 12px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--border-radius);
color: var(--light-text);
font-family: inherit;
margin-bottom: 16px;
resize: vertical;
}
.edit-memory-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.main-content {
margin-right: 400px;
transition: margin-right 0.3s ease;
max-width: 800px;
}
.main-content.sidebar-collapsed {
margin-right: 0;
}
#status-container {
margin-bottom: 12px;
}

View File

@@ -0,0 +1,259 @@
:root {
--dark-bg: #1a1a1a;
--darker-bg: #121212;
--light-text: #f1f1f1;
--blue-accent: #3d84f7;
--blue-hover: #2d74e7;
--blue-light: rgba(61, 132, 247, 0.15);
--error-color: #ff4a4a;
--warning-color: #ffaa33;
--success-color: #4caf50;
--border-radius: 8px;
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
font-family: "Roboto", -apple-system, BlinkMacSystemFont, sans-serif;
width: 320px;
margin: 0;
padding: 0;
color: var(--light-text);
background-color: var(--dark-bg);
}
header {
background-color: var(--darker-bg);
color: var(--light-text);
padding: 16px;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
h1 {
font-size: 18px;
margin: 0 0 8px 0;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
.logo-img {
height: 16px;
width: auto;
margin-left: 8px;
position: relative;
top: 1px;
}
.powered-by {
font-size: 12px;
font-weight: normal;
color: rgba(255, 255, 255, 0.6);
line-height: 1;
}
.branding-container {
display: flex;
align-items: center;
justify-content: center;
margin-top: 4px;
}
.content {
padding: 16px;
}
.status {
padding: 12px;
border-radius: var(--border-radius);
margin-bottom: 16px;
font-size: 14px;
animation: fade-in 0.3s ease;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.status.error {
background-color: rgba(255, 74, 74, 0.1);
color: var(--error-color);
border: 1px solid rgba(255, 74, 74, 0.2);
}
.status.success {
background-color: rgba(76, 175, 80, 0.1);
color: var(--success-color);
border: 1px solid rgba(76, 175, 80, 0.2);
}
.status.warning {
background-color: rgba(255, 170, 51, 0.1);
color: var(--warning-color);
border: 1px solid rgba(255, 170, 51, 0.2);
}
button {
background-color: var(--blue-accent);
color: white;
border: none;
padding: 12px 16px;
border-radius: 6px;
cursor: pointer;
width: 100%;
font-size: 14px;
font-weight: 500;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
button:hover {
background-color: var(--blue-hover);
transform: translateY(-1px);
}
button:active {
transform: translateY(1px);
}
button:disabled {
background-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.4);
cursor: not-allowed;
transform: none;
}
.actions {
display: flex;
flex-direction: row;
gap: 12px;
}
.api-key-section {
margin-bottom: 20px;
position: relative;
}
.api-key-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.toggle-password {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: rgba(255, 255, 255, 0.5);
width: auto;
display: flex;
align-items: center;
justify-content: center;
}
.toggle-password:hover {
color: rgba(255, 255, 255, 0.8);
background: none;
transform: translateY(-50%);
}
.toggle-password .icon {
width: 16px;
height: 16px;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 12px;
padding-right: 40px;
background-color: rgba(255, 255, 255, 0.05);
color: var(--light-text);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--border-radius);
margin-top: 6px;
box-sizing: border-box;
transition: var(--transition);
font-size: 14px;
}
input[type="text"]:focus,
input[type="password"]:focus {
border-color: var(--blue-accent);
outline: none;
box-shadow: 0 0 0 1px rgba(61, 132, 247, 0.2);
}
input::placeholder {
color: rgba(255, 255, 255, 0.3);
}
label {
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
display: block;
margin-bottom: 4px;
}
.save-button {
margin-top: 10px;
}
.mem0-status {
margin-top: 20px;
padding: 12px;
background-color: rgba(255, 255, 255, 0.03);
border-radius: var(--border-radius);
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
}
.mem0-status p {
margin: 0;
}
#mem0-status-text {
color: var(--blue-accent);
font-weight: 500;
}
/* Icons */
.icon {
display: inline-block;
width: 18px;
height: 18px;
fill: currentColor;
}
.get-key-link {
color: var(--blue-accent);
text-decoration: none;
font-size: 13px;
transition: color 0.2s ease;
}
.get-key-link:hover {
color: var(--blue-accent-hover);
text-decoration: underline;
}
.get-key-link:visited {
color: var(--blue-accent);
}

View File

@@ -0,0 +1,40 @@
const path = require('path');
module.exports = {
mode: 'production',
entry: {
content: './src/content.js',
options: './src/options.js',
popup: './src/popup.js',
background: './src/background.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
devtool: 'source-map',
optimization: {
minimize: false
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
resolve: {
extensions: ['.js']
}
};