mirror of
https://github.com/tcsenpai/spacellama.git
synced 2025-06-07 11:45:33 +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 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1,7 @@
|
|||||||
extension
|
extension
|
||||||
pack_extension.sh
|
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
|
# 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.
|
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
|
## Features
|
||||||
|
|
||||||
- **One-Click Summarization**: Quickly summarize any web page with a single click.
|
- **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.
|
- **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.
|
- **Markdown Rendering**: Summaries are rendered in Markdown for better readability and formatting.
|
||||||
- **Error Handling**: Robust error handling with informative messages for troubleshooting.
|
- **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
|
## 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.
|
1. Click the "Open Settings" button in the sidebar.
|
||||||
2. Set your preferred OLLAMA endpoint (default is `http://localhost:11434`).
|
2. Set your preferred OLLAMA endpoint (default is `http://localhost:11434`).
|
||||||
3. Choose the OLLAMA model you want to use (default is `llama2`).
|
3. Choose the OLLAMA model you want to use (default is `llama3.1:latest`).
|
||||||
4. Save your settings.
|
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
|
## 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.
|
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
|
## Contributing
|
||||||
|
|
||||||
Contributions to SpaceLLama are welcome! Please feel free to submit issues, feature requests, or pull requests to help improve the extension.
|
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).
|
Licensed under the [Do What The Fuck You Want To Public License](LICENSE.md).
|
||||||
See [LICENSE.md](LICENSE.md) for more details.
|
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.
|
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.
333
background.js
333
background.js
@ -1,55 +1,326 @@
|
|||||||
console.log("Background script loaded");
|
console.log("Background script loaded");
|
||||||
|
|
||||||
|
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(() => {
|
browser.browserAction.onClicked.addListener(() => {
|
||||||
|
console.log("Firefox: Toggling sidebar");
|
||||||
browser.sidebarAction.toggle();
|
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) => {
|
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
if (request.action === "summarize") {
|
if (request.action === "summarize") {
|
||||||
summarizeContent(request.content)
|
console.log("Summarization request received in background script.");
|
||||||
.then(summary => {
|
const tokenCount = estimateTokenCount(request.content);
|
||||||
sendResponse({ summary });
|
summarizeContent(request.content, request.systemPrompt)
|
||||||
|
.then((summary) => {
|
||||||
|
sendResponse({ summary, tokenCount });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('Error in summarizeContent:', error);
|
console.error("Error in summarizeContent:", error);
|
||||||
sendResponse({ error: error.toString(), details: error.details });
|
sendResponse({
|
||||||
|
error: error.toString(),
|
||||||
|
details: error.details,
|
||||||
|
tokenCount,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return true; // Indicates that we will send a response asynchronously
|
return true; // Indicates that we will send a response asynchronously
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function summarizeContent(content) {
|
async function summarizeContent(content, systemPrompt) {
|
||||||
const settings = await browser.storage.local.get(['ollamaEndpoint', 'ollamaModel']);
|
const settings = await browser.storage.local.get([
|
||||||
const endpoint = `${settings.ollamaEndpoint || 'http://localhost:11434'}/api/generate`;
|
"ollamaEndpoint",
|
||||||
const model = settings.ollamaModel || 'llama2';
|
"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 {
|
try {
|
||||||
const response = await fetch(endpoint, {
|
let { summary, chunkCount, recursionDepth } = await recursiveSummarize(
|
||||||
method: 'POST',
|
content,
|
||||||
headers: {
|
systemPrompt,
|
||||||
'Content-Type': 'application/json',
|
tokenLimit,
|
||||||
},
|
endpoint,
|
||||||
body: JSON.stringify({
|
model
|
||||||
prompt: `Summarize the following text:\n\n${content}`,
|
);
|
||||||
model: model,
|
console.log("Final summary completed.");
|
||||||
stream: false
|
return {
|
||||||
}),
|
summary:
|
||||||
});
|
typeof summary === "string" ? summary.trim() : JSON.stringify(summary),
|
||||||
|
// NOTE Chunk count and recursion depth are disabled if not needed
|
||||||
if (!response.ok) {
|
//chunkCount,
|
||||||
const errorText = await response.text();
|
//recursionDepth,
|
||||||
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return data.response;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error details:', error);
|
console.error("Error in summarizeContent:", error);
|
||||||
error.details = {
|
error.details = {
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
model: model,
|
model: model,
|
||||||
message: error.message
|
message: error.message,
|
||||||
};
|
};
|
||||||
throw error;
|
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 @@
|
|||||||
|
extensionLog("Content script loading...");
|
||||||
|
|
||||||
|
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() {
|
function getPageContent() {
|
||||||
console.log("getPageContent called");
|
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;
|
return document.body.innerText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up message listener
|
||||||
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
console.log("Content script received message:", request);
|
extensionLog("Content script received message:", request);
|
||||||
|
|
||||||
if (request.action === "getContent") {
|
if (request.action === "getContent") {
|
||||||
const content = getPageContent();
|
extensionLog("Getting page content...");
|
||||||
console.log("Sending content (first 100 chars):", content.substring(0, 100));
|
|
||||||
|
// 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 });
|
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
|
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
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Content script loaded");
|
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,14 +1,9 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "SpaceLLama",
|
"name": "SpaceLLama",
|
||||||
"version": "1.0",
|
"version": "1.7",
|
||||||
"description": "Summarize web pages using OLLAMA",
|
"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": [
|
"permissions": ["activeTab", "storage", "<all_urls>", "tabs"],
|
||||||
"activeTab",
|
|
||||||
"storage",
|
|
||||||
"<all_urls>",
|
|
||||||
"tabs"
|
|
||||||
],
|
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"default_title": "SpaceLLama",
|
"default_title": "SpaceLLama",
|
||||||
"default_icon": "icon.png"
|
"default_icon": "icon.png"
|
||||||
@ -32,7 +27,5 @@
|
|||||||
"page": "options/options.html",
|
"page": "options/options.html",
|
||||||
"open_in_tab": true
|
"open_in_tab": true
|
||||||
},
|
},
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": ["sidebar/marked.min.js", "model_tokens.json"]
|
||||||
"sidebar/marked.min.js"
|
|
||||||
]
|
|
||||||
}
|
}
|
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,11 +1,47 @@
|
|||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 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;
|
background-color: #f5f5f5;
|
||||||
color: #333;
|
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 {
|
.container {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@ -88,3 +124,17 @@ input[type="text"]:focus {
|
|||||||
background-color: #e74c3c;
|
background-color: #e74c3c;
|
||||||
color: white;
|
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,10 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>OLLAMA Summarizer Settings</title>
|
<title>OLLAMA Summarizer Settings</title>
|
||||||
<link rel="stylesheet" type="text/css" href="options.css">
|
<link rel="stylesheet" type="text/css" href="options.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -13,15 +13,49 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="endpoint">OLLAMA Endpoint:</label>
|
<label for="endpoint">OLLAMA Endpoint:</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" id="endpoint" placeholder="http://localhost:11434">
|
<input
|
||||||
|
type="text"
|
||||||
|
id="endpoint"
|
||||||
|
placeholder="http://localhost:11434"
|
||||||
|
/>
|
||||||
<span id="endpoint-status" class="status-indicator"></span>
|
<span id="endpoint-status" class="status-indicator"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="model">OLLAMA Model:</label>
|
<label for="model">OLLAMA Model:</label>
|
||||||
<input type="text" id="model" placeholder="llama2">
|
<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>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Save Settings</button>
|
<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>
|
</form>
|
||||||
<div id="status" class="status-message"></div>
|
<div id="status" class="status-message"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,60 +1,124 @@
|
|||||||
|
let browser =
|
||||||
|
typeof chrome !== "undefined"
|
||||||
|
? chrome
|
||||||
|
: typeof browser !== "undefined"
|
||||||
|
? browser
|
||||||
|
: null;
|
||||||
|
|
||||||
async function validateEndpoint(endpoint) {
|
async function validateEndpoint(endpoint) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${endpoint}/api/tags`);
|
const response = await fetch(`${endpoint}/api/tags`);
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error validating endpoint:', error);
|
console.error("Error validating endpoint:", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateEndpointStatus(isValid) {
|
function updateEndpointStatus(isValid) {
|
||||||
const statusElement = document.getElementById('endpoint-status');
|
const statusElement = document.getElementById("endpoint-status");
|
||||||
statusElement.textContent = isValid ? '✅' : '❌';
|
statusElement.textContent = isValid ? "✅" : "❌";
|
||||||
statusElement.title = isValid ? 'Endpoint is valid' : 'Endpoint is invalid';
|
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) {
|
async function saveOptions(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const endpoint = document.getElementById('endpoint').value;
|
const endpoint = document.getElementById("endpoint").value;
|
||||||
const model = document.getElementById('model').value;
|
const model = document.getElementById("model").value;
|
||||||
const status = document.getElementById('status');
|
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
|
// Ensure the endpoint doesn't end with /api/generate
|
||||||
const cleanEndpoint = endpoint.replace(/\/api\/generate\/?$/, '');
|
const cleanEndpoint = endpoint.replace(/\/api\/generate\/?$/, "");
|
||||||
|
status.textContent = "Validating endpoint...";
|
||||||
status.textContent = 'Validating endpoint...';
|
try {
|
||||||
const isValid = await validateEndpoint(cleanEndpoint);
|
const isValid = await validateEndpoint(cleanEndpoint);
|
||||||
updateEndpointStatus(isValid);
|
updateEndpointStatus(isValid);
|
||||||
|
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
browser.storage.local.set({
|
await browser.storage.local.set({
|
||||||
ollamaEndpoint: cleanEndpoint,
|
ollamaEndpoint: cleanEndpoint,
|
||||||
ollamaModel: model
|
ollamaModel: model,
|
||||||
}).then(() => {
|
systemPrompt: systemPrompt,
|
||||||
status.textContent = 'Options saved and endpoint validated.';
|
tokenLimit: parseInt(tokenLimit),
|
||||||
setTimeout(() => {
|
youtubeApiKey: youtubeApiKey,
|
||||||
status.textContent = '';
|
|
||||||
}, 2000);
|
|
||||||
});
|
});
|
||||||
|
status.textContent = "Options saved and endpoint validated.";
|
||||||
|
setTimeout(() => {
|
||||||
|
status.textContent = "";
|
||||||
|
}, 2000);
|
||||||
} else {
|
} else {
|
||||||
status.textContent = 'Invalid endpoint. Please check the URL and try again.';
|
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() {
|
function restoreOptions() {
|
||||||
const result = await browser.storage.local.get(['ollamaEndpoint', 'ollamaModel']);
|
browser.storage.local.get(
|
||||||
const endpoint = result.ollamaEndpoint || 'http://localhost:11434';
|
{
|
||||||
document.getElementById('endpoint').value = endpoint;
|
ollamaEndpoint: "http://localhost:11434",
|
||||||
document.getElementById('model').value = result.ollamaModel || 'llama2';
|
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);
|
// Call to updateTokenLimit remains async
|
||||||
|
updateTokenLimit().then(() => {
|
||||||
|
validateEndpoint(result.ollamaEndpoint).then((isValid) => {
|
||||||
updateEndpointStatus(isValid);
|
updateEndpointStatus(isValid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', restoreOptions);
|
document.addEventListener("DOMContentLoaded", restoreOptions);
|
||||||
document.getElementById('settings-form').addEventListener('submit', saveOptions);
|
document
|
||||||
document.getElementById('endpoint').addEventListener('blur', async (e) => {
|
.getElementById("settings-form")
|
||||||
|
.addEventListener("submit", saveOptions);
|
||||||
|
document.getElementById("endpoint").addEventListener("blur", async (e) => {
|
||||||
const isValid = await validateEndpoint(e.target.value);
|
const isValid = await validateEndpoint(e.target.value);
|
||||||
updateEndpointStatus(isValid);
|
updateEndpointStatus(isValid);
|
||||||
|
|
||||||
|
document.getElementById("model").addEventListener("change", updateTokenLimit);
|
||||||
});
|
});
|
@ -1,11 +1,48 @@
|
|||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 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;
|
background-color: #f5f5f5;
|
||||||
color: #333;
|
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 {
|
.container {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@ -21,60 +58,105 @@ h1 {
|
|||||||
.btn {
|
.btn {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.3s ease;
|
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 {
|
.btn-primary {
|
||||||
background-color: #3498db;
|
background: linear-gradient(135deg, #0b132b 0%, #3a506b 100%);
|
||||||
color: white;
|
color: #5bc0be;
|
||||||
|
border: 1px solid #5bc0be;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.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 {
|
.btn-secondary {
|
||||||
background-color: #95a5a6;
|
background: rgba(30, 30, 30, 0.9);
|
||||||
color: white;
|
color: #c5c6c7;
|
||||||
|
border: 1px solid #45a29e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.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 {
|
.summary-container {
|
||||||
margin-top: 20px;
|
margin-top: 10px;
|
||||||
padding: 15px;
|
padding: 10px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#summary h1, #summary h2, #summary h3 {
|
#summary h1,
|
||||||
margin-top: 15px;
|
#summary h2,
|
||||||
margin-bottom: 10px;
|
#summary h3 {
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
#summary p {
|
#summary p {
|
||||||
|
margin-top: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
#summary ul, #summary ol {
|
#summary ul,
|
||||||
|
#summary ol {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#summary li {
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#summary code {
|
#summary code {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: "Courier New", Courier, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
#summary pre {
|
#summary pre {
|
||||||
@ -82,9 +164,45 @@ h1 {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: "Courier New", Courier, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
#open-options {
|
#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,9 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" type="text/css" href="sidebar.css">
|
<link rel="stylesheet" type="text/css" href="sidebar.css" />
|
||||||
<script src="marked.min.js"></script>
|
<script src="marked.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -11,7 +11,15 @@
|
|||||||
<h1>OLLAMA Summarizer</h1>
|
<h1>OLLAMA Summarizer</h1>
|
||||||
<button id="summarize" class="btn btn-primary">Summarize</button>
|
<button id="summarize" class="btn btn-primary">Summarize</button>
|
||||||
<div id="summary" class="summary-container"></div>
|
<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>
|
<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>
|
</div>
|
||||||
<script src="sidebar.js"></script>
|
<script src="sidebar.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,33 +1,156 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
let isFirefox = typeof InstallTrigger !== "undefined"; // Firefox has `InstallTrigger`
|
||||||
const summarizeButton = document.getElementById('summarize');
|
let browser = isFirefox ? window.browser : chrome;
|
||||||
const summaryDiv = document.getElementById('summary');
|
|
||||||
const openOptionsButton = document.getElementById('open-options');
|
|
||||||
|
|
||||||
summarizeButton.addEventListener('click', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
summaryDiv.innerHTML = '<p>Summarizing...</p>';
|
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;
|
summarizeButton.disabled = true;
|
||||||
|
|
||||||
|
// Get the current tab content
|
||||||
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||||
browser.tabs.sendMessage(tabs[0].id, { action: "getContent" }, (response) => {
|
// 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) {
|
if (browser.runtime.lastError) {
|
||||||
handleError('Error getting page content: ' + browser.runtime.lastError.message);
|
handleError(
|
||||||
|
"Error getting page content: " + browser.runtime.lastError.message
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && response.content) {
|
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(
|
browser.runtime.sendMessage(
|
||||||
{ action: "summarize", content: response.content },
|
{
|
||||||
|
action: "summarize",
|
||||||
|
content: response.content,
|
||||||
|
systemPrompt: customizedPrompt,
|
||||||
|
},
|
||||||
(response) => {
|
(response) => {
|
||||||
if (browser.runtime.lastError) {
|
if (browser.runtime.lastError) {
|
||||||
handleError('Error during summarization: ' + browser.runtime.lastError.message);
|
handleError(
|
||||||
|
"Error during summarization: " +
|
||||||
|
browser.runtime.lastError.message
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && response.summary) {
|
if (response && response.summary) {
|
||||||
// Render the Markdown content
|
let warningHtml = "";
|
||||||
summaryDiv.innerHTML = marked.parse(response.summary);
|
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) {
|
} else if (response && response.error) {
|
||||||
handleError(response.error, response.details);
|
handleError(response.error, response.details);
|
||||||
|
if (response.tokenCount) {
|
||||||
|
tokenCountDiv.textContent = `Token count: ${response.tokenCount}`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
handleError("Unexpected response from summarization");
|
handleError("Unexpected response from summarization");
|
||||||
}
|
}
|
||||||
@ -35,13 +158,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
handleError('Error: Could not retrieve page content.');
|
handleError("Error: Could not retrieve page content.");
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
openOptionsButton.addEventListener('click', () => {
|
openOptionsButton.addEventListener("click", () => {
|
||||||
browser.runtime.openOptionsPage();
|
browser.runtime.openOptionsPage();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -53,4 +177,106 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
summarizeButton.disabled = false;
|
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";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} 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