mirror of
https://github.com/tcsenpai/spacellama.git
synced 2025-06-07 03:35:31 +00:00
Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ba230d04ed | ||
![]() |
0d60dbceb1 | ||
![]() |
f868a40d1b | ||
![]() |
28be327e91 | ||
![]() |
bcfbfab42f | ||
![]() |
fad943d216 | ||
![]() |
a98379f9ee | ||
![]() |
3477478cfd | ||
![]() |
96a1882323 | ||
![]() |
ad363a946a | ||
![]() |
71278d18b3 | ||
![]() |
050a74adad | ||
![]() |
91ad5375b0 | ||
![]() |
c4deab61d9 | ||
![]() |
7ea7dc0cdf | ||
![]() |
da5ef7a6fe | ||
![]() |
9f98d7a5cf | ||
![]() |
877143acce | ||
![]() |
49b881bb30 | ||
![]() |
7ab58c548f | ||
![]() |
e8435556a3 | ||
![]() |
c0b77bb24e | ||
![]() |
57a28a117f | ||
![]() |
08394baa54 | ||
![]() |
6a01bb6024 | ||
![]() |
083da7f71a | ||
![]() |
cd20d4f5c0 | ||
![]() |
f90b62070d | ||
![]() |
0c7ed8afff | ||
![]() |
f6cb0eeca4 | ||
![]() |
710a9595ac | ||
![]() |
c4a969044a |
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,3 +1,7 @@
|
||||
extension
|
||||
pack_extension.sh
|
||||
SpaceLLama.zip
|
||||
SpaceLLama.zip
|
||||
yarn.lock
|
||||
dist/*.xpi
|
||||
dist/*.zip
|
||||
node_modules
|
||||
|
30
README.md
30
README.md
@ -1,7 +1,13 @@
|
||||
# SpaceLLama
|
||||
|
||||
[](https://justforfunnoreally.dev)
|
||||
|
||||
SpaceLLama is a powerful browser extension that leverages OLLAMA to provide quick and efficient web page summarization. It offers a seamless way to distill the essence of any web content, saving you time and enhancing your browsing experience.
|
||||
|
||||

|
||||
|
||||
**[Download it from the Mozilla Extensions store!](https://addons.mozilla.org/en-US/firefox/addon/spacellama/)**
|
||||
|
||||
## Features
|
||||
|
||||
- **One-Click Summarization**: Quickly summarize any web page with a single click.
|
||||
@ -9,6 +15,8 @@ SpaceLLama is a powerful browser extension that leverages OLLAMA to provide quic
|
||||
- **Customizable OLLAMA Settings**: Easily configure the OLLAMA endpoint and model through the options page.
|
||||
- **Markdown Rendering**: Summaries are rendered in Markdown for better readability and formatting.
|
||||
- **Error Handling**: Robust error handling with informative messages for troubleshooting.
|
||||
- **Token Limit Handling**: Ability to set a token limit for the summary.
|
||||
- **Recursive Summarization with Context Chunking**: Recursively summarizes content that exceeds the token limit by breaking it into smaller chunks, summarizing each chunk, and then combining the summaries to provide a more comprehensive summary (only if the token limit is exceeded).
|
||||
|
||||
## How It Works
|
||||
|
||||
@ -24,8 +32,18 @@ You can customize SpaceLLama's behavior through the options page:
|
||||
|
||||
1. Click the "Open Settings" button in the sidebar.
|
||||
2. Set your preferred OLLAMA endpoint (default is `http://localhost:11434`).
|
||||
3. Choose the OLLAMA model you want to use (default is `llama2`).
|
||||
4. Save your settings.
|
||||
3. Choose the OLLAMA model you want to use (default is `llama3.1:latest`).
|
||||
4. Set the token limit for the summary (default is `16384`).
|
||||
5. Set the system prompt for the summary (default is `You are a helpful AI assistant. Summarize the given text concisely, without leaving out informations. You should aim to give a summary that is highly factual, useful and rich but still shorter than the original content, while not being too short.`).
|
||||
6. Save your settings.
|
||||
|
||||
## Manual Installation
|
||||
|
||||
1. Clone the repository.
|
||||
2. Install `npm install -g web-ext`
|
||||
3. Run `chmod +x build_xpi_webext.sh` to make the script executable.
|
||||
4. Run `./build_xpi_webext.sh` to build the extension.
|
||||
5. Install the extension in your browser through `about:debugging`
|
||||
|
||||
## Technical Details
|
||||
|
||||
@ -42,6 +60,10 @@ The extension uses the `marked` library to render Markdown content in the summar
|
||||
|
||||
SpaceLLama processes web page content locally through your configured OLLAMA endpoint. No data is sent to external servers beyond what you configure. Always ensure you're using a trusted OLLAMA setup, especially if using a remote endpoint.
|
||||
|
||||
## FAQ
|
||||
|
||||
- If you get a 403 error, you probably need to set the environment variable `OLLAMA_ORIGINS` to "\*" on your ollama server. On Windows, you will have to set the environment variable in the `SYSTEM` environment, not just the `USER` environment.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to SpaceLLama are welcome! Please feel free to submit issues, feature requests, or pull requests to help improve the extension.
|
||||
@ -51,6 +73,10 @@ Contributions to SpaceLLama are welcome! Please feel free to submit issues, feat
|
||||
Licensed under the [Do What The Fuck You Want To Public License](LICENSE.md).
|
||||
See [LICENSE.md](LICENSE.md) for more details.
|
||||
|
||||
## Credits
|
||||
|
||||
- [Background Image](https://www.pexels.com/) - I could not find the right image; if you know the author, please let me know so I can give them the credits they deserve.
|
||||
|
||||
---
|
||||
|
||||
SpaceLLama: Bringing the power of OLLAMA to your browser for effortless web page summarization.
|
||||
|
BIN
SpaceLlama.xpi
Normal file
BIN
SpaceLlama.xpi
Normal file
Binary file not shown.
341
background.js
341
background.js
@ -1,55 +1,326 @@
|
||||
console.log("Background script loaded");
|
||||
|
||||
browser.browserAction.onClicked.addListener(() => {
|
||||
browser.sidebarAction.toggle();
|
||||
});
|
||||
let isFirefox = typeof InstallTrigger !== "undefined"; // Firefox has `InstallTrigger`
|
||||
let browser = isFirefox ? window.browser : chrome;
|
||||
|
||||
// Check if chrome.action or browser.action is available
|
||||
if (isFirefox && browser.browserAction) {
|
||||
// Firefox specific: Use browserAction
|
||||
browser.browserAction.onClicked.addListener(() => {
|
||||
console.log("Firefox: Toggling sidebar");
|
||||
browser.sidebarAction.toggle();
|
||||
});
|
||||
} else if (browser.action) {
|
||||
// Chrome specific: Use action and inject the sidebar iframe
|
||||
browser.action.onClicked.addListener((tab) => {
|
||||
console.log("Injecting sidebar iframe into the page");
|
||||
|
||||
// Use the tab object properly here
|
||||
browser.scripting.executeScript(
|
||||
{
|
||||
target: { tabId: tab.id }, // Pass the tab ID correctly
|
||||
function: injectSidebar,
|
||||
},
|
||||
() => {
|
||||
if (browser.runtime.lastError) {
|
||||
console.error(
|
||||
"Error injecting sidebar:",
|
||||
browser.runtime.lastError.message
|
||||
);
|
||||
} else {
|
||||
console.log("Sidebar injected successfully.");
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to inject the sidebar as an iframe in browsers like Chrome
|
||||
function injectSidebar() {
|
||||
// Check if the sidebar iframe is already injected
|
||||
if (document.getElementById("sidebar-frame")) {
|
||||
console.log("Sidebar is already injected.");
|
||||
return;
|
||||
}
|
||||
// Create an iframe for the sidebar
|
||||
const sidebarFrame = document.createElement("iframe");
|
||||
sidebarFrame.id = "sidebar-frame"; // Add an ID to prevent multiple injections
|
||||
sidebarFrame.src = chrome.runtime.getURL("sidebar/sidebar.html"); // Use the sidebar.html
|
||||
sidebarFrame.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
z-index: 9999;
|
||||
background-color: white;
|
||||
`;
|
||||
|
||||
// Append the sidebar iframe to the body of the active webpage
|
||||
document.body.appendChild(sidebarFrame);
|
||||
}
|
||||
|
||||
// Background script listens for the 'summarize' action
|
||||
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === "summarize") {
|
||||
summarizeContent(request.content)
|
||||
.then(summary => {
|
||||
sendResponse({ summary });
|
||||
console.log("Summarization request received in background script.");
|
||||
const tokenCount = estimateTokenCount(request.content);
|
||||
summarizeContent(request.content, request.systemPrompt)
|
||||
.then((summary) => {
|
||||
sendResponse({ summary, tokenCount });
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error in summarizeContent:', error);
|
||||
sendResponse({ error: error.toString(), details: error.details });
|
||||
.catch((error) => {
|
||||
console.error("Error in summarizeContent:", error);
|
||||
sendResponse({
|
||||
error: error.toString(),
|
||||
details: error.details,
|
||||
tokenCount,
|
||||
});
|
||||
});
|
||||
return true; // Indicates that we will send a response asynchronously
|
||||
}
|
||||
});
|
||||
|
||||
async function summarizeContent(content) {
|
||||
const settings = await browser.storage.local.get(['ollamaEndpoint', 'ollamaModel']);
|
||||
const endpoint = `${settings.ollamaEndpoint || 'http://localhost:11434'}/api/generate`;
|
||||
const model = settings.ollamaModel || 'llama2';
|
||||
async function summarizeContent(content, systemPrompt) {
|
||||
const settings = await browser.storage.local.get([
|
||||
"ollamaEndpoint",
|
||||
"ollamaModel",
|
||||
"tokenLimit",
|
||||
]);
|
||||
const endpoint = `${
|
||||
settings.ollamaEndpoint || "http://localhost:11434"
|
||||
}/api/generate`;
|
||||
const model = settings.ollamaModel || "llama3.1:8b";
|
||||
const tokenLimit = settings.tokenLimit || 4096;
|
||||
|
||||
console.log(`Starting summarization process. Token limit: ${tokenLimit}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: `Summarize the following text:\n\n${content}`,
|
||||
model: model,
|
||||
stream: false
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.response;
|
||||
let { summary, chunkCount, recursionDepth } = await recursiveSummarize(
|
||||
content,
|
||||
systemPrompt,
|
||||
tokenLimit,
|
||||
endpoint,
|
||||
model
|
||||
);
|
||||
console.log("Final summary completed.");
|
||||
return {
|
||||
summary:
|
||||
typeof summary === "string" ? summary.trim() : JSON.stringify(summary),
|
||||
// NOTE Chunk count and recursion depth are disabled if not needed
|
||||
//chunkCount,
|
||||
//recursionDepth,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error details:', error);
|
||||
console.error("Error in summarizeContent:", error);
|
||||
error.details = {
|
||||
endpoint: endpoint,
|
||||
model: model,
|
||||
message: error.message
|
||||
message: error.message,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function recursiveSummarize(
|
||||
content,
|
||||
systemPrompt,
|
||||
tokenLimit,
|
||||
endpoint,
|
||||
model,
|
||||
depth = 0
|
||||
) {
|
||||
console.log(`Recursive summarization depth: ${depth}`);
|
||||
const chunks = splitContentIntoChunks(content, tokenLimit, systemPrompt);
|
||||
console.log(`Split content into ${chunks.length} chunks`);
|
||||
|
||||
let summaries = [];
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
console.log(`Summarizing chunk ${i + 1} of ${chunks.length}`);
|
||||
const chunkSummary = await summarizeChunk(
|
||||
chunks[i],
|
||||
systemPrompt,
|
||||
endpoint,
|
||||
model,
|
||||
tokenLimit
|
||||
);
|
||||
summaries.push(chunkSummary);
|
||||
}
|
||||
const combinedSummaries = summaries.join("\n\n");
|
||||
|
||||
if (chunks.length <= 1) {
|
||||
console.log("Single chunk, summarizing directly");
|
||||
return {
|
||||
summary: combinedSummaries,
|
||||
chunkCount: chunks.length,
|
||||
recursionDepth: depth,
|
||||
};
|
||||
} else {
|
||||
console.log("Multiple chunks, summarizing recursively");
|
||||
const result = await recursiveSummarize(
|
||||
combinedSummaries,
|
||||
systemPrompt,
|
||||
tokenLimit,
|
||||
endpoint,
|
||||
model,
|
||||
depth + 1
|
||||
);
|
||||
return {
|
||||
...result,
|
||||
chunkCount: chunks.length + result.chunkCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function summarizeChunk(
|
||||
chunk,
|
||||
systemPrompt,
|
||||
endpoint,
|
||||
model,
|
||||
tokenLimit
|
||||
) {
|
||||
let response;
|
||||
let maxRetries = 3;
|
||||
let retryCount = 0;
|
||||
let retryDelay = 1000;
|
||||
// We will retry the request if it fails (three times)
|
||||
// Each time we will wait longer before retrying (1, 2, 4 seconds)
|
||||
// Each request will timeout after 25 * retryDelay
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: `${systemPrompt}\n\nFollow the above instructions and summarize the following text:\n\n${chunk}`,
|
||||
model: model,
|
||||
stream: false,
|
||||
num_ctx: tokenLimit,
|
||||
}),
|
||||
signal: AbortSignal.timeout(25 * retryDelay),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
break; // Success - exit the retry loop
|
||||
} catch (error) {
|
||||
console.error("Error in summarizeChunk:", error);
|
||||
retryCount++;
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
throw new Error(
|
||||
`Failed to summarize chunk after ${maxRetries} retries: ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Retry ${retryCount}/${maxRetries} after ${retryDelay}ms`);
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||
retryDelay *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Add bespoke-minicheck validation here
|
||||
// LINK https://ollama.com/library/bespoke-minicheck
|
||||
let factCheck = false;
|
||||
if (factCheck) {
|
||||
let bespokeResponse = await bespokeMinicheck(chunk, summary);
|
||||
console.log(bespokeResponse);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.response;
|
||||
}
|
||||
|
||||
function estimateTokenCount(text) {
|
||||
return Math.ceil(text.length / 4);
|
||||
}
|
||||
|
||||
function splitContentIntoChunks(content, tokenLimit, systemPrompt) {
|
||||
const maxTokens = tokenLimit - estimateTokenCount(systemPrompt) - 100; // Reserve 100 tokens for safety
|
||||
const chunks = [];
|
||||
const words = content.split(/\s+/);
|
||||
let currentChunk = "";
|
||||
|
||||
for (const word of words) {
|
||||
if (estimateTokenCount(currentChunk + " " + word) > maxTokens) {
|
||||
chunks.push(currentChunk.trim());
|
||||
currentChunk = word;
|
||||
} else {
|
||||
currentChunk += (currentChunk ? " " : "") + word;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentChunk) {
|
||||
chunks.push(currentChunk.trim());
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
async function bespokeMinicheck(chunk, summary) {
|
||||
let bespoke_prompt = `
|
||||
Document: ${chunk}
|
||||
Claim: This is a correct summary of the document:\n\n ${summary},
|
||||
`;
|
||||
|
||||
let bespoke_body = {
|
||||
prompt: bespoke_prompt,
|
||||
model: "bespoke-minicheck:latest",
|
||||
stream: false,
|
||||
num_ctx: 30000, // Model is 32k but we want to leave some buffer
|
||||
options: {
|
||||
temperature: 0.0,
|
||||
num_predict: 2,
|
||||
},
|
||||
};
|
||||
|
||||
let bespoke_response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(bespoke_body),
|
||||
});
|
||||
// TODO Error handling
|
||||
let response_text = await bespoke_response.text();
|
||||
return response_text;
|
||||
}
|
||||
|
||||
// Add this to your background.js
|
||||
let extensionLogs = [];
|
||||
const MAX_LOGS = 1000;
|
||||
|
||||
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === "log") {
|
||||
// Store log
|
||||
extensionLogs.push({
|
||||
message: request.message,
|
||||
data: request.data,
|
||||
timestamp: request.timestamp,
|
||||
url: request.url,
|
||||
tabId: sender.tab ? sender.tab.id : "unknown",
|
||||
});
|
||||
|
||||
// Also log to the console
|
||||
console.log("[Content.js log]", request.message, request.data);
|
||||
|
||||
// Trim logs if they get too large
|
||||
if (extensionLogs.length > MAX_LOGS) {
|
||||
extensionLogs = extensionLogs.slice(-MAX_LOGS);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (request.action === "getLogs") {
|
||||
sendResponse({ logs: extensionLogs });
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle other messages...
|
||||
});
|
||||
|
26
build_xpi.sh
Executable file
26
build_xpi.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set extension name
|
||||
EXTENSION_NAME="SpaceLLama"
|
||||
|
||||
# Create a temporary directory for building
|
||||
BUILD_DIR="./build"
|
||||
mkdir -p $BUILD_DIR
|
||||
|
||||
# Copy all necessary files to the build directory
|
||||
echo "Copying files to build directory..."
|
||||
cp -r background.js content_scripts icon.png manifest.json options sidebar model_tokens.json $BUILD_DIR
|
||||
|
||||
# Navigate to the build directory
|
||||
cd $BUILD_DIR
|
||||
|
||||
# Create the XPI file (which is just a ZIP file with .xpi extension)
|
||||
echo "Creating XPI file..."
|
||||
zip -r ../${EXTENSION_NAME}.xpi *
|
||||
|
||||
# Clean up
|
||||
cd ..
|
||||
echo "Cleaning up build directory..."
|
||||
rm -rf $BUILD_DIR
|
||||
|
||||
echo "XPI file created: ${EXTENSION_NAME}.xpi"
|
6
build_xpi_webext.sh
Executable file
6
build_xpi_webext.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build the extension using web-ext
|
||||
web-ext build --source-dir ./ --artifacts-dir ./dist --overwrite-dest
|
||||
|
||||
echo "XPI file created in ./dist directory"
|
@ -1,16 +1,683 @@
|
||||
function getPageContent() {
|
||||
console.log("getPageContent called");
|
||||
return document.body.innerText;
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
console.log("Content script received message:", request);
|
||||
if (request.action === "getContent") {
|
||||
const content = getPageContent();
|
||||
console.log("Sending content (first 100 chars):", content.substring(0, 100));
|
||||
sendResponse({ content: content });
|
||||
}
|
||||
return true; // Indicate that we will send a response asynchronously
|
||||
});
|
||||
extensionLog("Content script loading...");
|
||||
|
||||
console.log("Content script loaded");
|
||||
let browser =
|
||||
typeof chrome !== "undefined"
|
||||
? chrome
|
||||
: typeof browser !== "undefined"
|
||||
? browser
|
||||
: null;
|
||||
|
||||
// Function to log messages to both console and background script
|
||||
function extensionLog(message, data = null) {
|
||||
// Log to console
|
||||
if (data) {
|
||||
console.log(`[SpaceLLama] ${message}`, data);
|
||||
} else {
|
||||
console.log(`[SpaceLLama] ${message}`);
|
||||
}
|
||||
|
||||
// Send to background script
|
||||
try {
|
||||
browser.runtime
|
||||
.sendMessage({
|
||||
action: "log",
|
||||
message: message,
|
||||
data: data,
|
||||
timestamp: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
})
|
||||
.catch((err) => console.error("Error sending log:", err));
|
||||
} catch (e) {
|
||||
console.error("Error in extensionLog:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// YouTube subtitle handler for SpaceLLama
|
||||
extensionLog("YouTube handler functionality initializing...");
|
||||
|
||||
// Function to check if the current page is a YouTube video
|
||||
function isYouTubeVideo(url) {
|
||||
return (
|
||||
url.includes("youtube.com/watch") ||
|
||||
url.includes("youtu.be/") ||
|
||||
url.includes("/watch?v=")
|
||||
);
|
||||
}
|
||||
|
||||
// Extract video ID from YouTube URL
|
||||
function extractVideoId(url) {
|
||||
let videoId = "";
|
||||
|
||||
if (url.includes("youtube.com/watch")) {
|
||||
const urlParams = new URLSearchParams(new URL(url).search);
|
||||
videoId = urlParams.get("v");
|
||||
} else if (url.includes("youtu.be/")) {
|
||||
videoId = url.split("youtu.be/")[1].split("?")[0];
|
||||
} else if (url.includes("/watch?v=")) {
|
||||
// Adding youtube.com to the URL if it's missing (e.g. invidious)
|
||||
url = "https://youtube.com/watch?v=" + url.split("/watch?v=")[1];
|
||||
const urlParams = new URLSearchParams(new URL(url).search);
|
||||
videoId = urlParams.get("v");
|
||||
}
|
||||
|
||||
return videoId;
|
||||
}
|
||||
|
||||
// Add this function to fetch subtitles using YouTube API
|
||||
async function fetchSubtitlesWithApi(videoId) {
|
||||
try {
|
||||
// Get the API key from storage
|
||||
const result = await browser.storage.sync.get({ youtubeApiKey: "" });
|
||||
const apiKey = result.youtubeApiKey;
|
||||
|
||||
if (!apiKey) {
|
||||
extensionLog("No YouTube API key provided in settings");
|
||||
return null;
|
||||
}
|
||||
|
||||
extensionLog("Attempting to fetch captions with YouTube API");
|
||||
|
||||
// First, get the caption tracks available for this video
|
||||
const captionListUrl = `https://www.googleapis.com/youtube/v3/captions?part=snippet&videoId=${videoId}&key=${apiKey}`;
|
||||
|
||||
const response = await fetch(captionListUrl);
|
||||
if (!response.ok) {
|
||||
extensionLog("YouTube API request failed:", response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.items || data.items.length === 0) {
|
||||
extensionLog("No caption tracks found via API");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find Eglish captions or use the first available
|
||||
const englishCaption =
|
||||
data.items.find(
|
||||
(item) =>
|
||||
item.snippet.language === "en" ||
|
||||
item.snippet.language === "en-US" ||
|
||||
(item.snippet.name &&
|
||||
item.snippet.name.toLowerCase().includes("english"))
|
||||
) || data.items[0];
|
||||
|
||||
// Get the caption content
|
||||
const captionId = englishCaption.id;
|
||||
const captionUrl = `https://www.googleapis.com/youtube/v3/captions/${captionId}?key=${apiKey}`;
|
||||
|
||||
// Note: This might require OAuth2 authentication which is beyond the scope of a simple extension
|
||||
// If this fails, we'll need to fall back to other methods
|
||||
const captionResponse = await fetch(captionUrl);
|
||||
if (!captionResponse.ok) {
|
||||
extensionLog("Failed to fetch caption content:", captionResponse.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const captionData = await captionResponse.json();
|
||||
return captionData.text;
|
||||
} catch (error) {
|
||||
extensionLog("Error fetching subtitles with API:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the fetchYouTubeSubtitles function to try the API first
|
||||
async function fetchYouTubeSubtitles(videoId) {
|
||||
extensionLog("Attempting to fetch subtitles for video ID:", videoId);
|
||||
try {
|
||||
// First try: Use YouTube API if key is provided
|
||||
const apiSubtitles = await fetchSubtitlesWithApi(videoId);
|
||||
if (apiSubtitles) {
|
||||
extensionLog("Successfully fetched subtitles using YouTube API");
|
||||
return apiSubtitles;
|
||||
}
|
||||
|
||||
// Method 1: Try to get subtitles directly from the page
|
||||
const subtitlesFromPage = getSubtitlesFromPage();
|
||||
if (subtitlesFromPage) {
|
||||
extensionLog("Found subtitles in page");
|
||||
return subtitlesFromPage;
|
||||
}
|
||||
|
||||
// If all methods fail, don't use fallbacks anymore
|
||||
extensionLog("No subtitles found, will use description only");
|
||||
return null;
|
||||
} catch (error) {
|
||||
extensionLog("Error fetching YouTube subtitles:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract player response data from page
|
||||
function getPlayerResponseData() {
|
||||
try {
|
||||
// YouTube stores player data in a script tag or window variable
|
||||
for (const script of document.querySelectorAll("script")) {
|
||||
if (script.textContent.includes("ytInitialPlayerResponse")) {
|
||||
const match = script.textContent.match(
|
||||
/ytInitialPlayerResponse\s*=\s*({.+?});/
|
||||
);
|
||||
if (match && match[1]) {
|
||||
return JSON.parse(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try window variable if available
|
||||
if (typeof window.ytInitialPlayerResponse !== "undefined") {
|
||||
return window.ytInitialPlayerResponse;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
extensionLog("Error getting player response data:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get initial player response from window variable
|
||||
function getInitialPlayerResponse() {
|
||||
try {
|
||||
// Look for the data in various possible locations
|
||||
if (typeof window.ytInitialPlayerResponse !== "undefined") {
|
||||
return window.ytInitialPlayerResponse;
|
||||
}
|
||||
|
||||
// Try to find it in script tags
|
||||
for (const script of document.querySelectorAll("script:not([src])")) {
|
||||
if (script.textContent.includes("ytInitialPlayerResponse")) {
|
||||
const match = script.textContent.match(
|
||||
/ytInitialPlayerResponse\s*=\s*({.+?});/
|
||||
);
|
||||
if (match && match[1]) {
|
||||
try {
|
||||
return JSON.parse(match[1]);
|
||||
} catch (e) {
|
||||
extensionLog("Error parsing ytInitialPlayerResponse:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
extensionLog("Error getting initial player response:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract subtitles from player response data
|
||||
function extractSubtitlesFromPlayerResponse(playerResponse) {
|
||||
try {
|
||||
if (!playerResponse || !playerResponse.captions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const captionTracks =
|
||||
playerResponse.captions?.playerCaptionsTracklistRenderer?.captionTracks;
|
||||
if (!captionTracks || captionTracks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find English subtitles or use the first available
|
||||
const englishTrack =
|
||||
captionTracks.find(
|
||||
(track) =>
|
||||
track.languageCode === "en" ||
|
||||
(track.name &&
|
||||
track.name.simpleText &&
|
||||
track.name.simpleText.includes("English"))
|
||||
) || captionTracks[0];
|
||||
|
||||
if (englishTrack && englishTrack.baseUrl) {
|
||||
// We found a subtitle track URL, but direct fetch might be restricted
|
||||
// For now, we'll extract what we can from the page
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
extensionLog("Error extracting subtitles from player response:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract subtitles from initial player response
|
||||
function extractSubtitlesFromInitialResponse(initialResponse) {
|
||||
try {
|
||||
if (!initialResponse) return null;
|
||||
|
||||
// Navigate through the complex structure to find captions
|
||||
const captionTracks =
|
||||
initialResponse.captions?.playerCaptionsTracklistRenderer?.captionTracks;
|
||||
if (!captionTracks || captionTracks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find English track or use first available
|
||||
const englishTrack =
|
||||
captionTracks.find(
|
||||
(track) =>
|
||||
track.languageCode === "en" ||
|
||||
(track.name?.simpleText && track.name.simpleText.includes("English"))
|
||||
) || captionTracks[0];
|
||||
|
||||
if (!englishTrack || !englishTrack.baseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We have a URL but can't directly fetch it due to CORS
|
||||
// Instead, extract what we can from the visible transcript on the page
|
||||
return null;
|
||||
} catch (error) {
|
||||
extensionLog("Error extracting subtitles from initial response:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get alternative text from video description or comments
|
||||
function getAlternativeText() {
|
||||
try {
|
||||
// Try to get the video description
|
||||
const description =
|
||||
document.querySelector("#description-inline-expander, #description-text")
|
||||
?.textContent || "";
|
||||
|
||||
// Try to get the transcript panel content if it's open
|
||||
const transcriptPanel = document.querySelector(".ytd-transcript-renderer");
|
||||
if (transcriptPanel) {
|
||||
const transcriptItems = transcriptPanel.querySelectorAll(
|
||||
".ytd-transcript-segment-renderer"
|
||||
);
|
||||
if (transcriptItems && transcriptItems.length > 0) {
|
||||
let transcript = "";
|
||||
transcriptItems.forEach((item) => {
|
||||
transcript += item.textContent + " ";
|
||||
});
|
||||
return transcript.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// If description is substantial, use it
|
||||
if (description.length > 200) {
|
||||
extensionLog(
|
||||
"FALLBACK: Using video description as alternative to transcript",
|
||||
{ descriptionLength: description.length }
|
||||
);
|
||||
return (
|
||||
"[FALLBACK: Using video description as alternative to transcript]\n\n" +
|
||||
description
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
extensionLog("Error getting alternative text:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find auto-generated captions
|
||||
function findAutoGeneratedCaptions() {
|
||||
try {
|
||||
// Check if the transcript button is available
|
||||
const transcriptButton = document.querySelector(
|
||||
'button[aria-label*="transcript"], button[aria-label*="Transcript"]'
|
||||
);
|
||||
if (transcriptButton) {
|
||||
// We can't click it programmatically due to security restrictions
|
||||
// But we can inform the user that transcripts are available
|
||||
return "Auto-generated captions may be available. Please click the transcript button in the YouTube player to view them.";
|
||||
}
|
||||
|
||||
// Look for any visible caption elements
|
||||
const visibleCaptions = document.querySelector(".ytp-caption-segment");
|
||||
if (visibleCaptions) {
|
||||
return "Captions are enabled for this video. Please ensure captions are turned on in the YouTube player to see them.";
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
extensionLog("Error finding auto-generated captions:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract text from the visible transcript panel if it's open
|
||||
function getVisibleTranscript() {
|
||||
try {
|
||||
// This targets the transcript panel that appears when you click "Show Transcript"
|
||||
const transcriptItems = document.querySelectorAll(
|
||||
"ytd-transcript-segment-renderer"
|
||||
);
|
||||
if (transcriptItems && transcriptItems.length > 0) {
|
||||
let transcript = "";
|
||||
transcriptItems.forEach((item) => {
|
||||
// Each segment has text and timestamp
|
||||
const text = item.querySelector("#text")?.textContent || "";
|
||||
transcript += text + " ";
|
||||
});
|
||||
return transcript.trim();
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
extensionLog("Error getting visible transcript:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse subtitles XML into plain text
|
||||
function parseSubtitlesXml(xmlText) {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
|
||||
const textElements = xmlDoc.getElementsByTagName("text");
|
||||
|
||||
let subtitles = "";
|
||||
for (let i = 0; i < textElements.length; i++) {
|
||||
subtitles += textElements[i].textContent + " ";
|
||||
}
|
||||
|
||||
return subtitles.trim();
|
||||
}
|
||||
|
||||
// Try to get subtitles directly from the YouTube page
|
||||
function getSubtitlesFromPage() {
|
||||
extensionLog("Attempting to get subtitles from page");
|
||||
|
||||
// First try: Check for transcript panel if it's open
|
||||
const visibleTranscript = getVisibleTranscript();
|
||||
if (visibleTranscript) {
|
||||
extensionLog("Found visible transcript panel");
|
||||
return visibleTranscript;
|
||||
}
|
||||
|
||||
// Second try: YouTube stores caption data in a script tag
|
||||
try {
|
||||
const scriptTags = document.querySelectorAll("script");
|
||||
let captionData = null;
|
||||
let captionTrackData = null;
|
||||
|
||||
// Look for caption tracks in script tags
|
||||
for (const script of scriptTags) {
|
||||
const content = script.textContent;
|
||||
|
||||
// Try different patterns to find caption data
|
||||
if (content.includes('"captionTracks"')) {
|
||||
const match = content.match(/"captionTracks":(\[.*?\])/);
|
||||
if (match && match[1]) {
|
||||
try {
|
||||
captionData = JSON.parse(match[1]);
|
||||
extensionLog("Found caption tracks in script tag");
|
||||
break;
|
||||
} catch (e) {
|
||||
extensionLog("Error parsing caption data:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try another pattern
|
||||
if (content.includes('{"captionTracks":')) {
|
||||
const regex = /{"captionTracks":(\[.*?\])}/g;
|
||||
const match = regex.exec(content);
|
||||
if (match && match[1]) {
|
||||
try {
|
||||
captionTrackData = JSON.parse(match[1]);
|
||||
extensionLog("Found caption track data in script tag");
|
||||
break;
|
||||
} catch (e) {
|
||||
extensionLog("Error parsing caption track data:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process caption data if found
|
||||
const tracks = captionData || captionTrackData;
|
||||
if (tracks && tracks.length > 0) {
|
||||
// Find English subtitles or use the first available
|
||||
const englishTrack =
|
||||
tracks.find(
|
||||
(track) =>
|
||||
track.languageCode === "en" ||
|
||||
(track.name &&
|
||||
track.name.simpleText &&
|
||||
track.name.simpleText.includes("English"))
|
||||
) || tracks[0];
|
||||
|
||||
if (englishTrack && englishTrack.baseUrl) {
|
||||
extensionLog("Found subtitle track URL, attempting to fetch");
|
||||
|
||||
// Try to fetch the subtitles directly (may fail due to CORS)
|
||||
try {
|
||||
return fetch(englishTrack.baseUrl)
|
||||
.then((response) => response.text())
|
||||
.then((xmlText) => {
|
||||
extensionLog("Successfully fetched subtitle XML");
|
||||
return parseSubtitlesXml(xmlText);
|
||||
})
|
||||
.catch((error) => {
|
||||
extensionLog("Error fetching subtitle XML:", error);
|
||||
return null;
|
||||
});
|
||||
} catch (error) {
|
||||
extensionLog("Error attempting to fetch subtitles:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Third try: Look for transcript in the page DOM
|
||||
const transcriptContent = document.querySelector("#transcript-scrollbox");
|
||||
if (transcriptContent) {
|
||||
extensionLog("Found transcript scrollbox");
|
||||
let transcript = "";
|
||||
const segments = transcriptContent.querySelectorAll(".segment");
|
||||
segments.forEach((segment) => {
|
||||
transcript += segment.textContent.trim() + " ";
|
||||
});
|
||||
|
||||
if (transcript.length > 100) {
|
||||
return transcript;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
extensionLog("Error getting subtitles from page:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get video metadata (title, description, etc.)
|
||||
function getVideoMetadata() {
|
||||
const title =
|
||||
document.querySelector('meta[property="og:title"]')?.content ||
|
||||
document.querySelector("title")?.textContent ||
|
||||
"";
|
||||
|
||||
const description =
|
||||
document.querySelector('meta[property="og:description"]')?.content ||
|
||||
document.querySelector('meta[name="description"]')?.content ||
|
||||
"";
|
||||
|
||||
const author =
|
||||
document.querySelector('link[itemprop="name"]')?.content ||
|
||||
document.querySelector(".ytd-channel-name a")?.textContent ||
|
||||
"";
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
author,
|
||||
};
|
||||
}
|
||||
|
||||
// Update getYouTubeContent to clearly indicate when using description
|
||||
async function getYouTubeContent() {
|
||||
extensionLog("getYouTubeContent called");
|
||||
const url = window.location.href;
|
||||
extensionLog("Current URL in getYouTubeContent:", url);
|
||||
|
||||
const videoId = extractVideoId(url);
|
||||
extensionLog("Extracted video ID:", videoId);
|
||||
|
||||
if (!videoId) {
|
||||
extensionLog("No video ID found");
|
||||
return null;
|
||||
}
|
||||
|
||||
extensionLog("Getting metadata and subtitles");
|
||||
const metadata = getVideoMetadata();
|
||||
extensionLog("Metadata retrieved:", metadata.title);
|
||||
|
||||
const subtitles = await fetchYouTubeSubtitles(videoId);
|
||||
extensionLog("Subtitles retrieved:", subtitles ? "yes" : "no");
|
||||
|
||||
if (!subtitles) {
|
||||
extensionLog("No subtitles available");
|
||||
// Get as much context as possible
|
||||
const videoLength = getVideoLength();
|
||||
const viewCount = getViewCount();
|
||||
const uploadDate = getUploadDate();
|
||||
|
||||
return {
|
||||
isYouTube: true,
|
||||
hasSubtitles: false,
|
||||
content:
|
||||
`Title: ${metadata.title}\n\n` +
|
||||
`Description: ${metadata.description}\n\n` +
|
||||
`Author: ${metadata.author}\n\n` +
|
||||
`Video Length: ${videoLength}\n` +
|
||||
`Views: ${viewCount}\n` +
|
||||
`Upload Date: ${uploadDate}\n\n` +
|
||||
`⚠️ NO TRANSCRIPT AVAILABLE: This summary is based only on the video metadata and description.`,
|
||||
};
|
||||
}
|
||||
|
||||
extensionLog("Returning YouTube content with subtitles");
|
||||
return {
|
||||
isYouTube: true,
|
||||
hasSubtitles: true,
|
||||
content: `Title: ${metadata.title}\n\nDescription: ${metadata.description}\n\nAuthor: ${metadata.author}\n\nTranscript:\n${subtitles}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper functions to get additional metadata
|
||||
function getVideoLength() {
|
||||
try {
|
||||
return (
|
||||
document.querySelector(".ytp-time-duration")?.textContent ||
|
||||
document.querySelector('span[itemprop="duration"]')?.textContent ||
|
||||
"Unknown"
|
||||
);
|
||||
} catch (e) {
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function getViewCount() {
|
||||
try {
|
||||
return (
|
||||
document.querySelector(".view-count")?.textContent ||
|
||||
document.querySelector('span[itemprop="interactionCount"]')
|
||||
?.textContent ||
|
||||
"Unknown"
|
||||
);
|
||||
} catch (e) {
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function getUploadDate() {
|
||||
try {
|
||||
return (
|
||||
document.querySelector("#info-strings yt-formatted-string")
|
||||
?.textContent ||
|
||||
document.querySelector('span[itemprop="datePublished"]')?.textContent ||
|
||||
"Unknown"
|
||||
);
|
||||
} catch (e) {
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// Main content script functionality
|
||||
function getPageContent() {
|
||||
extensionLog("getPageContent called");
|
||||
|
||||
// Check if we're on a YouTube video page
|
||||
const url = window.location.href;
|
||||
extensionLog("Current URL in getPageContent:", url);
|
||||
|
||||
if (isYouTubeVideo(url)) {
|
||||
extensionLog("YouTube video detected, fetching subtitles...");
|
||||
return getYouTubeContent()
|
||||
.then((youtubeContent) => {
|
||||
if (youtubeContent) {
|
||||
extensionLog(
|
||||
"YouTube content retrieved:",
|
||||
youtubeContent.hasSubtitles ? "with subtitles" : "without subtitles"
|
||||
);
|
||||
return youtubeContent.content;
|
||||
} else {
|
||||
// Fallback to regular page content if YouTube handler fails
|
||||
extensionLog(
|
||||
"YouTube handler failed, falling back to regular content"
|
||||
);
|
||||
return document.body.innerText;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
extensionLog("Error getting YouTube content:", error);
|
||||
return document.body.innerText;
|
||||
});
|
||||
}
|
||||
|
||||
// Regular page content for non-YouTube pages
|
||||
extensionLog("Not a YouTube video, returning regular page content");
|
||||
return document.body.innerText;
|
||||
}
|
||||
|
||||
// Set up message listener
|
||||
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
extensionLog("Content script received message:", request);
|
||||
|
||||
if (request.action === "getContent") {
|
||||
extensionLog("Getting page content...");
|
||||
|
||||
// Handle the case where getPageContent might return a Promise
|
||||
const contentResult = getPageContent();
|
||||
|
||||
if (contentResult instanceof Promise) {
|
||||
contentResult
|
||||
.then((content) => {
|
||||
extensionLog(
|
||||
"Sending content (first 100 chars):",
|
||||
content.substring(0, 100)
|
||||
);
|
||||
sendResponse({ content: content });
|
||||
})
|
||||
.catch((error) => {
|
||||
extensionLog("Error getting page content:", error);
|
||||
sendResponse({
|
||||
content: "Error retrieving content: " + error.message,
|
||||
});
|
||||
});
|
||||
return true; // Indicate that we will send a response asynchronously
|
||||
} else {
|
||||
// Handle synchronous result
|
||||
const content = contentResult;
|
||||
extensionLog(
|
||||
"Sending content (first 100 chars):",
|
||||
content.substring(0, 100)
|
||||
);
|
||||
sendResponse({ content: content });
|
||||
return true; // Still need to return true for Firefox compatibility
|
||||
}
|
||||
}
|
||||
|
||||
return true; // Always return true to indicate we're handling the message
|
||||
});
|
||||
|
||||
extensionLog("Content script fully loaded with YouTube handler functionality");
|
||||
|
BIN
dist/spacellama.png
vendored
Normal file
BIN
dist/spacellama.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 552 KiB |
@ -1,38 +1,31 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "SpaceLLama",
|
||||
"version": "1.0",
|
||||
"description": "Summarize web pages using OLLAMA",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"<all_urls>",
|
||||
"tabs"
|
||||
],
|
||||
"browser_action": {
|
||||
"default_title": "SpaceLLama",
|
||||
"default_icon": "icon.png"
|
||||
},
|
||||
"sidebar_action": {
|
||||
"default_title": "SpaceLLama",
|
||||
"default_panel": "sidebar/sidebar.html",
|
||||
"default_icon": "icon.png"
|
||||
},
|
||||
"background": {
|
||||
"scripts": ["background.js"],
|
||||
"persistent": false
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content_scripts/content.js"]
|
||||
}
|
||||
],
|
||||
"options_ui": {
|
||||
"page": "options/options.html",
|
||||
"open_in_tab": true
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
"sidebar/marked.min.js"
|
||||
]
|
||||
}
|
||||
"manifest_version": 2,
|
||||
"name": "SpaceLLama",
|
||||
"version": "1.7",
|
||||
"description": "Summarize web pages using Ollama. Supports custom models, token limits, system prompts, chunking, and more. See https://github.com/tcsenpai/spacellama for more information.",
|
||||
"permissions": ["activeTab", "storage", "<all_urls>", "tabs"],
|
||||
"browser_action": {
|
||||
"default_title": "SpaceLLama",
|
||||
"default_icon": "icon.png"
|
||||
},
|
||||
"sidebar_action": {
|
||||
"default_title": "SpaceLLama",
|
||||
"default_panel": "sidebar/sidebar.html",
|
||||
"default_icon": "icon.png"
|
||||
},
|
||||
"background": {
|
||||
"scripts": ["background.js"],
|
||||
"persistent": false
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content_scripts/content.js"]
|
||||
}
|
||||
],
|
||||
"options_ui": {
|
||||
"page": "options/options.html",
|
||||
"open_in_tab": true
|
||||
},
|
||||
"web_accessible_resources": ["sidebar/marked.min.js", "model_tokens.json"]
|
||||
}
|
||||
|
30
manifest_ch.json
Normal file
30
manifest_ch.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "SpaceLLama",
|
||||
"version": "1.21",
|
||||
"description": "Summarize web pages using Ollama. Supports custom models, token limits, system prompts, chunking, and more. See https://github.com/tcsenpai/spacellama for more information.",
|
||||
"permissions": ["activeTab", "storage", "tabs", "scripting"],
|
||||
"action": {
|
||||
"default_title": "SpaceLLama",
|
||||
"default_icon": "icon.png"
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["http://*/*", "https://*/*"],
|
||||
"js": ["content_scripts/youtube_handler.js", "content_scripts/content.js"]
|
||||
}
|
||||
],
|
||||
"options_ui": {
|
||||
"page": "options/options.html",
|
||||
"open_in_tab": true
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["sidebar/sidebar.html", "sidebar/sidebar.js", "sidebar/sidebar.css", "sidebar/marked.min.js", "model_tokens.json"],
|
||||
"matches": ["http://*/*", "https://*/*"]
|
||||
}
|
||||
]
|
||||
}
|
26
model_tokens.json
Normal file
26
model_tokens.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"llama2": 4096,
|
||||
"llama2:13b": 4096,
|
||||
"llama2:70b": 4096,
|
||||
"codellama": 16384,
|
||||
"codellama:13b": 16384,
|
||||
"codellama:34b": 16384,
|
||||
"mistral": 8192,
|
||||
"mixtral": 32768,
|
||||
"phi": 2048,
|
||||
"qwen": 8192,
|
||||
"qwen:14b": 8192,
|
||||
"qwen:72b": 8192,
|
||||
"stablelm": 4096,
|
||||
"stablelm-zephyr": 4096,
|
||||
"neural-chat": 8192,
|
||||
"openhermes": 8192,
|
||||
"starling-lm": 8192,
|
||||
"orca2": 4096,
|
||||
"vicuna": 8192,
|
||||
"wizardcoder": 16384,
|
||||
"wizardcoder:python": 16384,
|
||||
"wizardmath": 8192,
|
||||
"llama3.1:8b": 128000,
|
||||
"llama3.1:70b": 128000
|
||||
}
|
@ -1,90 +1,140 @@
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-image: url("https://static.pexels.com/photos/414171/pexels-photo-414171.jpeg");
|
||||
background-size: cover;
|
||||
-webkit-animation: slidein 100s;
|
||||
animation: slidein 100s;
|
||||
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards;
|
||||
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
|
||||
-webkit-animation-direction: alternate;
|
||||
animation-direction: alternate;
|
||||
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@-webkit-keyframes slidein {
|
||||
from {
|
||||
background-position: top;
|
||||
background-size: 3000px;
|
||||
}
|
||||
to {
|
||||
background-position: -100px 0px;
|
||||
background-size: 2750px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slidein {
|
||||
from {
|
||||
background-position: top;
|
||||
background-size: 3000px;
|
||||
}
|
||||
to {
|
||||
background-position: -100px 0px;
|
||||
background-size: 2750px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 30px;
|
||||
color: #2c3e50;
|
||||
font-size: 28px;
|
||||
margin-bottom: 30px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #34495e;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #bdc3c7;
|
||||
border-radius: 5px;
|
||||
transition: border-color 0.3s ease;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #bdc3c7;
|
||||
border-radius: 5px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
margin-left: 10px;
|
||||
font-size: 20px;
|
||||
margin-left: 10px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #2980b9;
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-message.success {
|
||||
background-color: #2ecc71;
|
||||
color: white;
|
||||
background-color: #2ecc71;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-message.error {
|
||||
background-color: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
background-color: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #bdc3c7;
|
||||
border-radius: 5px;
|
||||
transition: border-color 0.3s ease;
|
||||
resize: vertical;
|
||||
}
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
@ -1,30 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OLLAMA Summarizer Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="options.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>OLLAMA Summarizer Settings</h1>
|
||||
<form id="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="endpoint">OLLAMA Endpoint:</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="endpoint" placeholder="http://localhost:11434">
|
||||
<span id="endpoint-status" class="status-indicator"></span>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OLLAMA Summarizer Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="options.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>OLLAMA Summarizer Settings</h1>
|
||||
<form id="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="endpoint">OLLAMA Endpoint:</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="endpoint"
|
||||
placeholder="http://localhost:11434"
|
||||
/>
|
||||
<span id="endpoint-status" class="status-indicator"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="model">OLLAMA Model:</label>
|
||||
<input type="text" id="model" placeholder="llama2">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||
</form>
|
||||
<div id="status" class="status-message"></div>
|
||||
</div>
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<div class="form-group">
|
||||
<label for="model">OLLAMA Model:</label>
|
||||
<input type="text" id="model" placeholder="llama3.1:latest" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="token-limit">Token Limit:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="token-limit"
|
||||
min="1024"
|
||||
placeholder="16384"
|
||||
/>
|
||||
</div>
|
||||
<div class="option-group">
|
||||
<label for="youtube-api-key">YouTube API Key (optional):</label>
|
||||
<input
|
||||
type="text"
|
||||
id="youtube-api-key"
|
||||
name="youtube-api-key"
|
||||
placeholder="Enter your YouTube API key"
|
||||
/>
|
||||
<p class="help-text">
|
||||
Used to fetch YouTube video transcripts.
|
||||
<a
|
||||
href="https://developers.google.com/youtube/v3/getting-started"
|
||||
target="_blank"
|
||||
>Get a key</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||
<div class="form-group">
|
||||
<label for="system-prompt">System Prompt:</label>
|
||||
<textarea id="system-prompt" rows="3" class="form-control"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
<div id="status" class="status-message"></div>
|
||||
</div>
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,60 +1,124 @@
|
||||
let browser =
|
||||
typeof chrome !== "undefined"
|
||||
? chrome
|
||||
: typeof browser !== "undefined"
|
||||
? browser
|
||||
: null;
|
||||
|
||||
async function validateEndpoint(endpoint) {
|
||||
try {
|
||||
const response = await fetch(`${endpoint}/api/tags`);
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error validating endpoint:', error);
|
||||
console.error("Error validating endpoint:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateEndpointStatus(isValid) {
|
||||
const statusElement = document.getElementById('endpoint-status');
|
||||
statusElement.textContent = isValid ? '✅' : '❌';
|
||||
statusElement.title = isValid ? 'Endpoint is valid' : 'Endpoint is invalid';
|
||||
const statusElement = document.getElementById("endpoint-status");
|
||||
statusElement.textContent = isValid ? "✅" : "❌";
|
||||
statusElement.title = isValid ? "Endpoint is valid" : "Endpoint is invalid";
|
||||
}
|
||||
|
||||
async function updateTokenLimit() {
|
||||
try {
|
||||
const modelTokens = await loadModelTokens();
|
||||
const model = document.getElementById("model").value;
|
||||
const tokenLimitInput = document.getElementById("token-limit");
|
||||
|
||||
if (model in modelTokens) {
|
||||
tokenLimitInput.value = modelTokens[model];
|
||||
} else {
|
||||
tokenLimitInput.value = 16384; // Default value
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating token limit:", error.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadModelTokens() {
|
||||
try {
|
||||
const response = await fetch(browser.runtime.getURL("model_tokens.json"));
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error loading model tokens:", error.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOptions(e) {
|
||||
e.preventDefault();
|
||||
const endpoint = document.getElementById('endpoint').value;
|
||||
const model = document.getElementById('model').value;
|
||||
const status = document.getElementById('status');
|
||||
|
||||
const endpoint = document.getElementById("endpoint").value;
|
||||
const model = document.getElementById("model").value;
|
||||
const systemPrompt = document.getElementById("system-prompt").value;
|
||||
const status = document.getElementById("status");
|
||||
const tokenLimit = document.getElementById("token-limit").value || 16384;
|
||||
const youtubeApiKey = document.getElementById("youtube-api-key").value;
|
||||
// Ensure the endpoint doesn't end with /api/generate
|
||||
const cleanEndpoint = endpoint.replace(/\/api\/generate\/?$/, '');
|
||||
|
||||
status.textContent = 'Validating endpoint...';
|
||||
const isValid = await validateEndpoint(cleanEndpoint);
|
||||
updateEndpointStatus(isValid);
|
||||
|
||||
if (isValid) {
|
||||
browser.storage.local.set({
|
||||
ollamaEndpoint: cleanEndpoint,
|
||||
ollamaModel: model
|
||||
}).then(() => {
|
||||
status.textContent = 'Options saved and endpoint validated.';
|
||||
const cleanEndpoint = endpoint.replace(/\/api\/generate\/?$/, "");
|
||||
status.textContent = "Validating endpoint...";
|
||||
try {
|
||||
const isValid = await validateEndpoint(cleanEndpoint);
|
||||
updateEndpointStatus(isValid);
|
||||
if (isValid) {
|
||||
await browser.storage.local.set({
|
||||
ollamaEndpoint: cleanEndpoint,
|
||||
ollamaModel: model,
|
||||
systemPrompt: systemPrompt,
|
||||
tokenLimit: parseInt(tokenLimit),
|
||||
youtubeApiKey: youtubeApiKey,
|
||||
});
|
||||
status.textContent = "Options saved and endpoint validated.";
|
||||
setTimeout(() => {
|
||||
status.textContent = '';
|
||||
status.textContent = "";
|
||||
}, 2000);
|
||||
});
|
||||
} else {
|
||||
status.textContent = 'Invalid endpoint. Please check the URL and try again.';
|
||||
} else {
|
||||
status.textContent =
|
||||
"Invalid endpoint. Please check the URL and try again.";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving options:", error.message || error);
|
||||
status.textContent = "Error saving options.";
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreOptions() {
|
||||
const result = await browser.storage.local.get(['ollamaEndpoint', 'ollamaModel']);
|
||||
const endpoint = result.ollamaEndpoint || 'http://localhost:11434';
|
||||
document.getElementById('endpoint').value = endpoint;
|
||||
document.getElementById('model').value = result.ollamaModel || 'llama2';
|
||||
function restoreOptions() {
|
||||
browser.storage.local.get(
|
||||
{
|
||||
ollamaEndpoint: "http://localhost:11434",
|
||||
ollamaModel: "llama3.1:latest",
|
||||
systemPrompt:
|
||||
"You are a helpful AI assistant. Summarize the given text concisely, without leaving out informations. You should aim to give a summary that is highly factual, useful and rich but still shorter than the original content, while not being too short.",
|
||||
tokenLimit: 16384,
|
||||
youtubeApiKey: "",
|
||||
},
|
||||
function (result) {
|
||||
document.getElementById("endpoint").value =
|
||||
result.ollamaEndpoint || "http://localhost:11434";
|
||||
document.getElementById("model").value =
|
||||
result.ollamaModel || "llama3.1:latest";
|
||||
document.getElementById("system-prompt").value =
|
||||
result.systemPrompt ||
|
||||
"You are a helpful AI assistant. Summarize the given text concisely.";
|
||||
document.getElementById("youtube-api-key").value = result.youtubeApiKey;
|
||||
|
||||
const isValid = await validateEndpoint(endpoint);
|
||||
updateEndpointStatus(isValid);
|
||||
// Call to updateTokenLimit remains async
|
||||
updateTokenLimit().then(() => {
|
||||
validateEndpoint(result.ollamaEndpoint).then((isValid) => {
|
||||
updateEndpointStatus(isValid);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', restoreOptions);
|
||||
document.getElementById('settings-form').addEventListener('submit', saveOptions);
|
||||
document.getElementById('endpoint').addEventListener('blur', async (e) => {
|
||||
document.addEventListener("DOMContentLoaded", restoreOptions);
|
||||
document
|
||||
.getElementById("settings-form")
|
||||
.addEventListener("submit", saveOptions);
|
||||
document.getElementById("endpoint").addEventListener("blur", async (e) => {
|
||||
const isValid = await validateEndpoint(e.target.value);
|
||||
updateEndpointStatus(isValid);
|
||||
});
|
||||
|
||||
document.getElementById("model").addEventListener("change", updateTokenLimit);
|
||||
});
|
||||
|
@ -1,90 +1,208 @@
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
background-image: url("https://static.pexels.com/photos/414171/pexels-photo-414171.jpeg");
|
||||
background-size: cover;
|
||||
-webkit-animation: slidein 100s;
|
||||
animation: slidein 100s;
|
||||
|
||||
-webkit-animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards;
|
||||
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
|
||||
-webkit-animation-direction: alternate;
|
||||
animation-direction: alternate;
|
||||
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@-webkit-keyframes slidein {
|
||||
from {
|
||||
background-position: top;
|
||||
background-size: 3000px;
|
||||
}
|
||||
to {
|
||||
background-position: -100px 0px;
|
||||
background-size: 2750px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slidein {
|
||||
from {
|
||||
background-position: top;
|
||||
background-size: 3000px;
|
||||
}
|
||||
to {
|
||||
background-position: -100px 0px;
|
||||
background-size: 2750px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
color: #2c3e50;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent
|
||||
);
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #0b132b 0%, #3a506b 100%);
|
||||
color: #5bc0be;
|
||||
border: 1px solid #5bc0be;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #2980b9;
|
||||
background: linear-gradient(135deg, #0b132b 0%, #3a506b 100%);
|
||||
box-shadow: 0 5px 15px rgba(91, 192, 190, 0.4),
|
||||
0 0 5px rgba(91, 192, 190, 0.4);
|
||||
color: #6fffe9;
|
||||
border-color: #6fffe9;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #95a5a6;
|
||||
color: white;
|
||||
background: rgba(30, 30, 30, 0.9);
|
||||
color: #c5c6c7;
|
||||
border: 1px solid #45a29e;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #7f8c8d;
|
||||
background: rgba(30, 30, 30, 0.9);
|
||||
box-shadow: 0 5px 15px rgba(69, 162, 158, 0.3),
|
||||
0 0 5px rgba(69, 162, 158, 0.3);
|
||||
color: #ffffff;
|
||||
border-color: #66fcf1;
|
||||
}
|
||||
|
||||
.summary-container {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#summary h1, #summary h2, #summary h3 {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
color: #2c3e50;
|
||||
#summary h1,
|
||||
#summary h2,
|
||||
#summary h3 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
#summary p {
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.6;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#summary ul, #summary ol {
|
||||
padding-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
#summary ul,
|
||||
#summary ol {
|
||||
padding-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#summary li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#summary code {
|
||||
background-color: #f0f0f0;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
background-color: #f0f0f0;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
#summary pre {
|
||||
background-color: #f0f0f0;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
background-color: #f0f0f0;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
overflow-x: auto;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
#open-options {
|
||||
margin-top: 20px;
|
||||
}
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
#save-prompt {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#summary,
|
||||
#system-prompt {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
}
|
||||
|
||||
#summary {
|
||||
/* white-space: pre-wrap; */
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
@ -1,18 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" type="text/css" href="sidebar.css">
|
||||
<script src="marked.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>OLLAMA Summarizer</h1>
|
||||
<button id="summarize" class="btn btn-primary">Summarize</button>
|
||||
<div id="summary" class="summary-container"></div>
|
||||
<button id="open-options" class="btn btn-secondary">Open Settings</button>
|
||||
</div>
|
||||
<script src="sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" type="text/css" href="sidebar.css" />
|
||||
<script src="marked.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>OLLAMA Summarizer</h1>
|
||||
<button id="summarize" class="btn btn-primary">Summarize</button>
|
||||
<div id="summary" class="summary-container"></div>
|
||||
<div class="form-group">
|
||||
<label for="system-prompt">System Prompt:</label>
|
||||
<textarea id="system-prompt" rows="3" class="form-control"></textarea>
|
||||
</div>
|
||||
<button id="save-prompt" class="btn btn-secondary">Save Prompt</button>
|
||||
<button id="open-options" class="btn btn-secondary">Open Settings</button>
|
||||
<div class="button-container">
|
||||
<button id="view-logs" class="button">View Debug Logs</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="sidebar.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,33 +1,156 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const summarizeButton = document.getElementById('summarize');
|
||||
const summaryDiv = document.getElementById('summary');
|
||||
const openOptionsButton = document.getElementById('open-options');
|
||||
|
||||
summarizeButton.addEventListener('click', () => {
|
||||
summaryDiv.innerHTML = '<p>Summarizing...</p>';
|
||||
summarizeButton.disabled = true;
|
||||
|
||||
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
browser.tabs.sendMessage(tabs[0].id, { action: "getContent" }, (response) => {
|
||||
let isFirefox = typeof InstallTrigger !== "undefined"; // Firefox has `InstallTrigger`
|
||||
let browser = isFirefox ? window.browser : chrome;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const summarizeButton = document.getElementById("summarize");
|
||||
const summaryDiv = document.getElementById("summary");
|
||||
const openOptionsButton = document.getElementById("open-options");
|
||||
const tokenCountDiv = document.createElement("div");
|
||||
tokenCountDiv.id = "token-count";
|
||||
tokenCountDiv.style.marginTop = "10px";
|
||||
tokenCountDiv.style.fontStyle = "italic";
|
||||
|
||||
summarizeButton.parentNode.insertBefore(
|
||||
tokenCountDiv,
|
||||
summarizeButton.nextSibling
|
||||
);
|
||||
// Correctly define systemPromptTextarea
|
||||
const systemPromptTextarea = document.getElementById("system-prompt");
|
||||
|
||||
summarizeButton.addEventListener("click", () => {
|
||||
summaryDiv.innerHTML = "<p>Summarizing...</p>";
|
||||
console.log("Summarizing...");
|
||||
tokenCountDiv.textContent = "";
|
||||
summarizeButton.disabled = true;
|
||||
|
||||
// Get the current tab content
|
||||
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
// First check if the current URL is a YouTube video
|
||||
const currentUrl = tabs[0].url;
|
||||
console.log("Current URL:", currentUrl);
|
||||
const isYouTubeUrl =
|
||||
currentUrl.includes("youtube.com/watch") ||
|
||||
currentUrl.includes("youtu.be/") ||
|
||||
currentUrl.includes("/watch?v=");
|
||||
console.log("Is YouTube URL:", isYouTubeUrl);
|
||||
|
||||
if (isYouTubeUrl) {
|
||||
// Show a notification that we're processing a YouTube video
|
||||
summaryDiv.innerHTML = `
|
||||
<div style="background-color: #1a73e8; color: white; padding: 10px; margin-bottom: 10px; border-radius: 4px;">
|
||||
<strong>YouTube Video Detected!</strong><br>
|
||||
Fetching and processing video transcript...
|
||||
</div>
|
||||
<p>Summarizing video content...</p>
|
||||
`;
|
||||
}
|
||||
|
||||
browser.tabs.sendMessage(
|
||||
tabs[0].id,
|
||||
{ action: "getContent" },
|
||||
(response) => {
|
||||
console.log("Response:", response);
|
||||
if (browser.runtime.lastError) {
|
||||
handleError('Error getting page content: ' + browser.runtime.lastError.message);
|
||||
handleError(
|
||||
"Error getting page content: " + browser.runtime.lastError.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (response && response.content) {
|
||||
const systemPrompt = systemPromptTextarea.value;
|
||||
var failedToFetchSubtitles = false;
|
||||
// Check if the content appears to be from YouTube
|
||||
var isYouTubeContent =
|
||||
response.content.includes("Title:") &&
|
||||
response.content.includes("Transcript:") &&
|
||||
(tabs[0].url.includes("youtube.com") ||
|
||||
tabs[0].url.includes("youtu.be") ||
|
||||
tabs[0].url.includes("/watch?v="));
|
||||
console.log("Is YouTube Content:", isYouTubeContent);
|
||||
if (response.content.includes("NO TRANSCRIPT AVAILABLE")) {
|
||||
console.log(
|
||||
"Warning: No subtitles available for this video: setting isYouTubeContent to false"
|
||||
);
|
||||
failedToFetchSubtitles = true;
|
||||
isYouTubeContent = false;
|
||||
}
|
||||
// Customize the prompt for YouTube videos
|
||||
const customizedPrompt = isYouTubeContent
|
||||
? `${systemPrompt}\n\nThis is a YouTube video transcript. Please summarize the key points discussed in the video.`
|
||||
: systemPrompt;
|
||||
console.log("System prompt:", customizedPrompt);
|
||||
|
||||
// Send message to background script for summarization
|
||||
browser.runtime.sendMessage(
|
||||
{ action: "summarize", content: response.content },
|
||||
{
|
||||
action: "summarize",
|
||||
content: response.content,
|
||||
systemPrompt: customizedPrompt,
|
||||
},
|
||||
(response) => {
|
||||
if (browser.runtime.lastError) {
|
||||
handleError('Error during summarization: ' + browser.runtime.lastError.message);
|
||||
handleError(
|
||||
"Error during summarization: " +
|
||||
browser.runtime.lastError.message
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (response && response.summary) {
|
||||
// Render the Markdown content
|
||||
summaryDiv.innerHTML = marked.parse(response.summary);
|
||||
let warningHtml = "";
|
||||
if (response.chunkCount > 1) {
|
||||
warningHtml = `
|
||||
<div class="warning" style="background-color: #fff3cd; color: #856404; padding: 10px; margin-bottom: 10px; border-radius: 4px;">
|
||||
<strong>Warning:</strong> The content was split into ${response.chunkCount} chunks for summarization.
|
||||
Recursive summarization depth: ${response.recursionDepth}.
|
||||
This may affect the quality and coherence of the summary, and might result in slower performance.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (failedToFetchSubtitles) {
|
||||
warningHtml =
|
||||
`
|
||||
<div class="warning" style="background-color: #fff3cd; color: #856404; padding: 10px; margin-bottom: 10px; border-radius: 4px;">
|
||||
<strong>Warning:</strong> Failed to fetch subtitles for this video: using video description and metadata instead.
|
||||
</div>
|
||||
` + warningHtml;
|
||||
}
|
||||
|
||||
// Add YouTube notification if it's a YouTube video
|
||||
if (isYouTubeContent) {
|
||||
warningHtml =
|
||||
`
|
||||
<div style="background-color: #1a73e8; color: white; padding: 10px; margin-bottom: 10px; border-radius: 4px;">
|
||||
<strong>YouTube Video Summary</strong><br>
|
||||
This summary was generated from the video transcript.
|
||||
</div>
|
||||
` + warningHtml;
|
||||
}
|
||||
|
||||
let summaryText;
|
||||
if (typeof response.summary === "string") {
|
||||
summaryText = response.summary;
|
||||
} else if (typeof response.summary === "object") {
|
||||
// Convert JSON to Markdown
|
||||
summaryText = Object.entries(response.summary)
|
||||
.map(([key, value]) => `## ${key}\n\n${value}`)
|
||||
.join("\n\n");
|
||||
} else {
|
||||
summaryText = JSON.stringify(response.summary);
|
||||
}
|
||||
|
||||
// Render the Markdown content with warning if applicable
|
||||
summaryDiv.innerHTML =
|
||||
warningHtml + marked.parse(summaryText);
|
||||
// NOTE Token count is disabled if not needed
|
||||
//tokenCountDiv.textContent = `Token count: ${response.tokenCount}`;
|
||||
} else if (response && response.error) {
|
||||
handleError(response.error, response.details);
|
||||
if (response.tokenCount) {
|
||||
tokenCountDiv.textContent = `Token count: ${response.tokenCount}`;
|
||||
}
|
||||
} else {
|
||||
handleError("Unexpected response from summarization");
|
||||
}
|
||||
@ -35,22 +158,125 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
);
|
||||
} else {
|
||||
handleError('Error: Could not retrieve page content.');
|
||||
handleError("Error: Could not retrieve page content.");
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
openOptionsButton.addEventListener("click", () => {
|
||||
browser.runtime.openOptionsPage();
|
||||
});
|
||||
|
||||
function handleError(errorMessage, details = null) {
|
||||
console.error("Error:", errorMessage, details);
|
||||
summaryDiv.innerHTML = `<p>Error: ${errorMessage}</p>`;
|
||||
if (details) {
|
||||
summaryDiv.innerHTML += `<pre>${JSON.stringify(details, null, 2)}</pre>`;
|
||||
}
|
||||
summarizeButton.disabled = false;
|
||||
}
|
||||
|
||||
const viewLogsButton = document.getElementById("view-logs");
|
||||
|
||||
// Only add the event listener if the button exists
|
||||
if (viewLogsButton) {
|
||||
// Add the same CSS class as other buttons
|
||||
viewLogsButton.className = "button";
|
||||
|
||||
let logsVisible = false;
|
||||
|
||||
viewLogsButton.addEventListener("click", () => {
|
||||
const logsDiv = document.getElementById("logs-container");
|
||||
|
||||
// Toggle logs visibility
|
||||
if (logsVisible && logsDiv) {
|
||||
// Hide logs if they're currently visible
|
||||
logsDiv.remove();
|
||||
logsVisible = false;
|
||||
viewLogsButton.textContent = "View Debug Logs";
|
||||
return;
|
||||
}
|
||||
|
||||
// Show logs
|
||||
browser.runtime.sendMessage({ action: "getLogs" }, (response) => {
|
||||
if (response && response.logs) {
|
||||
// Remove existing logs container if it exists
|
||||
if (logsDiv) {
|
||||
logsDiv.remove();
|
||||
}
|
||||
|
||||
// Create new logs container
|
||||
const newLogsDiv = document.createElement("div");
|
||||
newLogsDiv.id = "logs-container";
|
||||
newLogsDiv.style.marginTop = "20px";
|
||||
newLogsDiv.style.borderTop = "1px solid #ccc";
|
||||
newLogsDiv.innerHTML = "<h3>Extension Logs</h3>";
|
||||
|
||||
const logList = document.createElement("pre");
|
||||
logList.style.maxHeight = "400px";
|
||||
logList.style.overflow = "auto";
|
||||
logList.style.whiteSpace = "pre-wrap";
|
||||
logList.style.fontSize = "12px";
|
||||
logList.style.backgroundColor = "#f5f5f5";
|
||||
logList.style.padding = "10px";
|
||||
logList.style.borderRadius = "4px";
|
||||
|
||||
// Add logs in reverse chronological order (newest first)
|
||||
response.logs
|
||||
.slice()
|
||||
.reverse()
|
||||
.forEach((log) => {
|
||||
const logEntry = document.createElement("div");
|
||||
logEntry.style.marginBottom = "5px";
|
||||
logEntry.style.borderBottom = "1px dotted #ddd";
|
||||
logEntry.style.paddingBottom = "5px";
|
||||
|
||||
// Highlight important logs
|
||||
if (
|
||||
log.message.includes("No subtitles") ||
|
||||
log.message.includes("Using alternative text") ||
|
||||
log.message.includes("Error")
|
||||
) {
|
||||
logEntry.style.color = log.message.includes("Error")
|
||||
? "#d32f2f"
|
||||
: "#ff9800";
|
||||
logEntry.style.fontWeight = "bold";
|
||||
}
|
||||
|
||||
logEntry.textContent = `[${log.timestamp}] ${log.message}`;
|
||||
if (log.data) {
|
||||
logEntry.textContent += ` ${JSON.stringify(log.data)}`;
|
||||
}
|
||||
logList.appendChild(logEntry);
|
||||
});
|
||||
|
||||
newLogsDiv.appendChild(logList);
|
||||
document.getElementById("summary").appendChild(newLogsDiv);
|
||||
logsVisible = true;
|
||||
viewLogsButton.textContent = "Hide Debug Logs";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
openOptionsButton.addEventListener('click', () => {
|
||||
browser.runtime.openOptionsPage();
|
||||
});
|
||||
|
||||
function handleError(errorMessage, details = null) {
|
||||
console.error("Error:", errorMessage, details);
|
||||
summaryDiv.innerHTML = `<p>Error: ${errorMessage}</p>`;
|
||||
if (details) {
|
||||
summaryDiv.innerHTML += `<pre>${JSON.stringify(details, null, 2)}</pre>`;
|
||||
}
|
||||
summarizeButton.disabled = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn("View logs button not found in the sidebar");
|
||||
}
|
||||
});
|
||||
|
||||
const systemPromptTextarea = document.getElementById("system-prompt");
|
||||
const savePromptButton = document.getElementById("save-prompt");
|
||||
|
||||
// Load saved system prompt
|
||||
browser.storage.local.get("systemPrompt").then((result) => {
|
||||
const defaultSystemPrompt =
|
||||
"You are a helpful AI assistant. Summarize the given text concisely.";
|
||||
systemPromptTextarea.value = result.systemPrompt || defaultSystemPrompt;
|
||||
});
|
||||
|
||||
savePromptButton.addEventListener("click", () => {
|
||||
const systemPrompt = systemPromptTextarea.value;
|
||||
browser.storage.local.set({ systemPrompt }).then(() => {
|
||||
alert("System prompt saved successfully!");
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user