Add YT assistant chrome extension (#2485)
This commit is contained in:
4
examples/yt-assistant-chrome/.gitignore
vendored
Normal file
4
examples/yt-assistant-chrome/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
.env*
|
||||
dist
|
||||
package-lock.json
|
||||
88
examples/yt-assistant-chrome/README.md
Normal file
88
examples/yt-assistant-chrome/README.md
Normal 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
|
||||
8
examples/yt-assistant-chrome/assets/dark.svg
Normal file
8
examples/yt-assistant-chrome/assets/dark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
45
examples/yt-assistant-chrome/manifest.json
Normal file
45
examples/yt-assistant-chrome/manifest.json
Normal 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/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
26
examples/yt-assistant-chrome/package.json
Normal file
26
examples/yt-assistant-chrome/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
196
examples/yt-assistant-chrome/public/options.html
Normal file
196
examples/yt-assistant-chrome/public/options.html
Normal 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">
|
||||
×
|
||||
</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>
|
||||
165
examples/yt-assistant-chrome/public/popup.html
Normal file
165
examples/yt-assistant-chrome/public/popup.html
Normal 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>
|
||||
255
examples/yt-assistant-chrome/src/background.js
Normal file
255
examples/yt-assistant-chrome/src/background.js
Normal 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 };
|
||||
},
|
||||
};
|
||||
}
|
||||
657
examples/yt-assistant-chrome/src/content.js
Normal file
657
examples/yt-assistant-chrome/src/content.js
Normal 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(/&#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 = `
|
||||
<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>';
|
||||
}
|
||||
}
|
||||
452
examples/yt-assistant-chrome/src/options.js
Normal file
452
examples/yt-assistant-chrome/src/options.js
Normal 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);
|
||||
}
|
||||
}
|
||||
241
examples/yt-assistant-chrome/src/popup.js
Normal file
241
examples/yt-assistant-chrome/src/popup.js
Normal 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);
|
||||
}
|
||||
}
|
||||
492
examples/yt-assistant-chrome/styles/content.css
Normal file
492
examples/yt-assistant-chrome/styles/content.css
Normal 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);
|
||||
}
|
||||
587
examples/yt-assistant-chrome/styles/options.css
Normal file
587
examples/yt-assistant-chrome/styles/options.css
Normal 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;
|
||||
}
|
||||
259
examples/yt-assistant-chrome/styles/popup.css
Normal file
259
examples/yt-assistant-chrome/styles/popup.css
Normal 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);
|
||||
}
|
||||
40
examples/yt-assistant-chrome/webpack.config.js
Normal file
40
examples/yt-assistant-chrome/webpack.config.js
Normal 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']
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user