v1.2
This commit is contained in:
commit
ffda90ce16
22
README.md
Normal file
22
README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# CAS
|
||||
|
||||
A simple, versatile language processing interface for websites.
|
||||
|
||||
## Features
|
||||
- Configurable
|
||||
- Use own API
|
||||
- Adjust opacity of responses
|
||||
- Hidden
|
||||
- Only visible to those who know where to look
|
||||
|
||||
## Usage
|
||||
After installing the extensions, selecting text and pressing `Ctrl+Space` will display the answer above the selected text in the configured opacity. To hide the answer, press `Ctrl+Space` again without selecting any text.
|
||||
Additionally, pressing `Ctrl+Shift+Space` will write the answer into the nearest text field.
|
||||
|
||||
## Configuration
|
||||
The extension can be configured by clicking on the extension icon in the toolbar.
|
||||
You will need to add your own OpenAI Endpoint (for example `https://api.elia.network`), and an API key.
|
||||
There is an default URL and Key which is restricted to llama3.2-90b and 10 Requests per minute.
|
||||
The opacity can be adjusted from 0 to 1, and is set to 0.1 per default. Adjust based on your screen's brightness and if you want to risk it being spotted.
|
||||
|
||||
## Installation
|
8
src/background.js
Normal file
8
src/background.js
Normal file
@ -0,0 +1,8 @@
|
||||
// background.js
|
||||
browser.commands.onCommand.addListener((command) => {
|
||||
if (command === "print-selection" || command === "fill-input") {
|
||||
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
|
||||
browser.tabs.sendMessage(tabs[0].id, { action: command });
|
||||
});
|
||||
}
|
||||
});
|
326
src/content.js
Normal file
326
src/content.js
Normal file
@ -0,0 +1,326 @@
|
||||
// Constants
|
||||
const DEFAULT_SETTINGS = {
|
||||
apiUrl: "https://api.elia.network",
|
||||
apiKey: "sk-TvFhtxHAPXEcmRtyva-ctA",
|
||||
textOpacity: 10,
|
||||
model: "llama3.2-90b",
|
||||
};
|
||||
|
||||
const MAX_CONTEXT_LENGTH = 6000;
|
||||
|
||||
// State
|
||||
let overlay = null;
|
||||
let isVisible = false;
|
||||
let lastKnownSelection = null;
|
||||
let lastKnownRange = null;
|
||||
let scrollTimeout = null;
|
||||
let resizeRAF = null;
|
||||
|
||||
// Utility Functions
|
||||
const getPageContext = () => {
|
||||
const bodyText = document.body.innerText;
|
||||
return bodyText.length > MAX_CONTEXT_LENGTH
|
||||
? `${bodyText.substring(0, MAX_CONTEXT_LENGTH)}...`
|
||||
: bodyText;
|
||||
};
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const data = await browser.storage.sync.get("settings");
|
||||
return data.settings || DEFAULT_SETTINGS;
|
||||
} catch (error) {
|
||||
console.error("Error loading settings:", error);
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
};
|
||||
|
||||
// API Functions
|
||||
const queryLLM = async (text) => {
|
||||
const settings = await loadSettings();
|
||||
const pageContext = getPageContext();
|
||||
const response = await fetch(`${settings.apiUrl}/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${settings.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: settings.model,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: `You are tasked to solve an exam. Exam's page context: ${pageContext}\n\Answer this question of said exam in very short but accurate manner (Utilize context to figure out the exam's topic. ONLY REPLY WITH ANSWER TO THE QUESTION): ${text}`,
|
||||
},
|
||||
],
|
||||
temperature: 0.1,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => null);
|
||||
throw new Error(
|
||||
`HTTP error! status: ${response.status}${errorData ? `, message: ${JSON.stringify(errorData)}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices[0].message.content;
|
||||
};
|
||||
|
||||
// UI Functions
|
||||
const createOverlay = (text) => {
|
||||
if (overlay) overlay.remove();
|
||||
|
||||
overlay = document.createElement("div");
|
||||
overlay.textContent = text;
|
||||
overlay.className = "llm-overlay";
|
||||
|
||||
loadSettings().then((settings) => {
|
||||
overlay.style.cssText = `
|
||||
position: absolute;
|
||||
padding: 10px;
|
||||
z-index: 2147483647;
|
||||
color: rgba(0, 0, 0, ${settings.textOpacity / 100});
|
||||
font-size: 14px;
|
||||
max-width: 600px;
|
||||
white-space: pre-wrap;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
requestAnimationFrame(positionOverlay);
|
||||
});
|
||||
|
||||
return overlay;
|
||||
};
|
||||
|
||||
const positionOverlay = () => {
|
||||
if (!overlay || !isVisible) return;
|
||||
|
||||
try {
|
||||
const selection = window.getSelection();
|
||||
if (!selection.rangeCount) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
// Get scroll positions
|
||||
const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
|
||||
const scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
// Get viewport dimensions
|
||||
const viewportWidth =
|
||||
window.innerWidth || document.documentElement.clientWidth;
|
||||
const viewportHeight =
|
||||
window.innerHeight || document.documentElement.clientHeight;
|
||||
|
||||
// Calculate overlay dimensions
|
||||
const overlayWidth = overlay.offsetWidth;
|
||||
const overlayHeight = overlay.offsetHeight;
|
||||
|
||||
// Calculate positions
|
||||
let top = rect.top + scrollY;
|
||||
let left = rect.left + scrollX + rect.width / 2 - overlayWidth / 2;
|
||||
|
||||
// Position above selection by default
|
||||
top -= overlayHeight + 5;
|
||||
|
||||
// If overlay would go above viewport, position it below selection
|
||||
if (top - scrollY < 0) {
|
||||
top = rect.bottom + scrollY + 5;
|
||||
}
|
||||
|
||||
// Keep overlay within horizontal bounds
|
||||
left = Math.max(
|
||||
scrollX + 5,
|
||||
Math.min(left, scrollX + viewportWidth - overlayWidth - 10),
|
||||
);
|
||||
|
||||
// Apply positions
|
||||
overlay.style.top = `${top}px`;
|
||||
overlay.style.left = `${left}px`;
|
||||
} catch (error) {
|
||||
console.error("Error positioning overlay:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Event Handlers
|
||||
const handleScroll = () => {
|
||||
if (!isVisible) return;
|
||||
|
||||
if (scrollTimeout) {
|
||||
cancelAnimationFrame(scrollTimeout);
|
||||
}
|
||||
|
||||
scrollTimeout = requestAnimationFrame(positionOverlay);
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
if (!isVisible) return;
|
||||
|
||||
if (resizeRAF) {
|
||||
cancelAnimationFrame(resizeRAF);
|
||||
}
|
||||
|
||||
resizeRAF = requestAnimationFrame(positionOverlay);
|
||||
};
|
||||
|
||||
// Initialize Event Listeners
|
||||
window.addEventListener("resize", handleResize, { passive: true });
|
||||
|
||||
const attachScrollListeners = () => {
|
||||
// Listen to window scroll
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
|
||||
// Listen to scroll events on all scrollable elements
|
||||
document.addEventListener("scroll", handleScroll, {
|
||||
capture: true,
|
||||
passive: true,
|
||||
});
|
||||
|
||||
// Find and attach listeners to all scrollable containers
|
||||
const scrollableElements = document.querySelectorAll(
|
||||
[
|
||||
'*[style*="overflow: auto"]',
|
||||
'*[style*="overflow:auto"]',
|
||||
'*[style*="overflow-y: auto"]',
|
||||
'*[style*="overflow-y:auto"]',
|
||||
'*[style*="overflow: scroll"]',
|
||||
'*[style*="overflow:scroll"]',
|
||||
'*[style*="overflow-y: scroll"]',
|
||||
'*[style*="overflow-y:scroll"]',
|
||||
].join(","),
|
||||
);
|
||||
|
||||
scrollableElements.forEach((element) => {
|
||||
element.addEventListener("scroll", handleScroll, { passive: true });
|
||||
});
|
||||
|
||||
// Handle dynamic content changes
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.addedNodes.length) {
|
||||
const newScrollableElements = document.querySelectorAll(
|
||||
[
|
||||
'*[style*="overflow: auto"]',
|
||||
'*[style*="overflow:auto"]',
|
||||
'*[style*="overflow-y: auto"]',
|
||||
'*[style*="overflow-y:auto"]',
|
||||
'*[style*="overflow: scroll"]',
|
||||
'*[style*="overflow:scroll"]',
|
||||
'*[style*="overflow-y: scroll"]',
|
||||
'*[style*="overflow-y:scroll"]',
|
||||
].join(","),
|
||||
);
|
||||
|
||||
newScrollableElements.forEach((element) => {
|
||||
if (!element.hasScrollListener) {
|
||||
element.addEventListener("scroll", handleScroll, { passive: true });
|
||||
element.hasScrollListener = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize scroll listeners
|
||||
attachScrollListeners();
|
||||
|
||||
// Message Handler
|
||||
browser.runtime.onMessage.addListener((message) => {
|
||||
if (message.action === "print-selection") {
|
||||
toggleOverlay();
|
||||
} else if (message.action === "fill-input") {
|
||||
modifyNearestInput();
|
||||
}
|
||||
});
|
||||
|
||||
// Main Functions
|
||||
const toggleOverlay = async () => {
|
||||
const selection = window.getSelection();
|
||||
const selectedText = selection.toString().trim();
|
||||
|
||||
if (!selectedText) {
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
overlay = null;
|
||||
}
|
||||
isVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isVisible = true;
|
||||
createOverlay("Processing...");
|
||||
|
||||
try {
|
||||
const llmResponse = await queryLLM(selectedText);
|
||||
createOverlay(llmResponse);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
createOverlay("Error processing request");
|
||||
}
|
||||
};
|
||||
|
||||
function findNearestInput(selectionNode) {
|
||||
const inputs = Array.from(
|
||||
document.querySelectorAll(
|
||||
'input[type="text"], input:not([type]), textarea',
|
||||
),
|
||||
);
|
||||
|
||||
if (!inputs.length) return null;
|
||||
|
||||
const selection = window.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
const rect = range.getBoundingClientRect();
|
||||
const selectionX = rect.left + rect.width / 2;
|
||||
const selectionY = rect.top + rect.height / 2;
|
||||
|
||||
let nearestInput = null;
|
||||
let shortestDistance = Infinity;
|
||||
|
||||
inputs.forEach((input) => {
|
||||
const inputRect = input.getBoundingClientRect();
|
||||
const inputX = inputRect.left + inputRect.width / 2;
|
||||
const inputY = inputRect.top + inputRect.height / 2;
|
||||
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(selectionX - inputX, 2) + Math.pow(selectionY - inputY, 2),
|
||||
);
|
||||
|
||||
if (distance < shortestDistance) {
|
||||
shortestDistance = distance;
|
||||
nearestInput = input;
|
||||
}
|
||||
});
|
||||
|
||||
return nearestInput;
|
||||
}
|
||||
|
||||
const modifyNearestInput = async () => {
|
||||
const selection = window.getSelection();
|
||||
const selectedText = selection.toString().trim();
|
||||
if (!selectedText) return;
|
||||
|
||||
const nearestInput = findNearestInput(selection.anchorNode);
|
||||
if (!nearestInput) {
|
||||
alert("No input field found nearby");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const oldPlaceholder = nearestInput.placeholder;
|
||||
nearestInput.placeholder = "Processing...";
|
||||
const llmResponse = await queryLLM(selectedText);
|
||||
nearestInput.value = llmResponse;
|
||||
nearestInput.placeholder = oldPlaceholder;
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
nearestInput.value = "Error processing request";
|
||||
}
|
||||
};
|
BIN
src/icons/icon128.png
Normal file
BIN
src/icons/icon128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
src/icons/icon16.png
Normal file
BIN
src/icons/icon16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 373 B |
BIN
src/icons/icon32.png
Normal file
BIN
src/icons/icon32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
BIN
src/icons/icon48.png
Normal file
BIN
src/icons/icon48.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
49
src/manifest.json
Normal file
49
src/manifest.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "CAS",
|
||||
"version": "1.2",
|
||||
"description": "Language processing interface",
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "cas@elia.network",
|
||||
"strict_min_version": "42.0"
|
||||
}
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"]
|
||||
}
|
||||
],
|
||||
"permissions": ["activeTab", "storage", "*://api.elia.network/*"],
|
||||
"commands": {
|
||||
"print-selection": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Space"
|
||||
},
|
||||
"description": "Process question"
|
||||
},
|
||||
"fill-input": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+Space"
|
||||
},
|
||||
"description": "Process question and fill into nearest input field"
|
||||
}
|
||||
},
|
||||
"browser_action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
}
|
268
src/popup.html
Normal file
268
src/popup.html
Normal file
@ -0,0 +1,268 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>CAS</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #818cf8;
|
||||
--primary-hover: #6366f1;
|
||||
--bg: #0f172a;
|
||||
--surface: #1e293b;
|
||||
--surface-hover: #334155;
|
||||
--border: #334155;
|
||||
--text: #f8fafc;
|
||||
--text-secondary: #94a3b8;
|
||||
--input-bg: #1e293b;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 320px;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
transition: all 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(129, 140, 248, 0.1);
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
-webkit-appearance: none;
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--surface);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
min-width: 36px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.shortcuts-panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.shortcut:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.shortcut-keys {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.key {
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
font-family: ui-monospace, monospace;
|
||||
color: var(--text-secondary);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.save-button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: var(--primary);
|
||||
color: var(--text);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.save-button:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #065f46;
|
||||
color: #ecfdf5;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #991b1b;
|
||||
color: #fef2f2;
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h2>CAS</h2>
|
||||
<span class="version-badge">v1.2</span>
|
||||
</div>
|
||||
|
||||
<form id="settingsForm">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="apiUrl">API URL</label>
|
||||
<input type="url" id="apiUrl" class="input" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="apiKey">API Key</label>
|
||||
<input type="password" id="apiKey" class="input" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="model">Model</label>
|
||||
<input id="model" class="input" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Answer Text Style</label>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
id="textOpacity"
|
||||
class="slider"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value="50"
|
||||
/>
|
||||
<span id="opacityValue" class="slider-value">50%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-panel">
|
||||
<div class="shortcut">
|
||||
<span>Solve Question</span>
|
||||
<div class="shortcut-keys">
|
||||
<span class="key">⌘</span>
|
||||
<span class="key">⇧</span>
|
||||
<span class="key">U</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut">
|
||||
<span>Fill Answer</span>
|
||||
<div class="shortcut-keys">
|
||||
<span class="key">⌘</span>
|
||||
<span class="key">⇧</span>
|
||||
<span class="key">7</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="save-button">Save Settings</button>
|
||||
</form>
|
||||
|
||||
<div id="status"></div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
80
src/popup.js
Normal file
80
src/popup.js
Normal file
@ -0,0 +1,80 @@
|
||||
const DEFAULT_SETTINGS = {
|
||||
apiUrl: "https://api.elia.network",
|
||||
apiKey: "sk-TvFhtxHAPXEcmRtyva-ctA",
|
||||
textOpacity: 10,
|
||||
model: "llama3.2-90b",
|
||||
};
|
||||
|
||||
// Load settings when the popup opens
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
try {
|
||||
const settings = await loadSettings();
|
||||
|
||||
// Populate form fields
|
||||
document.getElementById("apiUrl").value = settings.apiUrl;
|
||||
document.getElementById("model").value = settings.model;
|
||||
document.getElementById("apiKey").value = settings.apiKey;
|
||||
document.getElementById("textOpacity").value = settings.textOpacity;
|
||||
document.getElementById("opacityValue").textContent =
|
||||
`${settings.textOpacity}%`;
|
||||
} catch (error) {
|
||||
console.error("Error loading settings:", error);
|
||||
showStatus("Error loading settings", "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Handle opacity slider changes
|
||||
document.getElementById("textOpacity").addEventListener("input", (e) => {
|
||||
document.getElementById("opacityValue").textContent = `${e.target.value}%`;
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
document
|
||||
.getElementById("settingsForm")
|
||||
.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const settings = {
|
||||
apiUrl: document.getElementById("apiUrl").value,
|
||||
apiKey: document.getElementById("apiKey").value,
|
||||
model: document.getElementById("model").value,
|
||||
textOpacity: parseInt(document.getElementById("textOpacity").value),
|
||||
};
|
||||
|
||||
try {
|
||||
await browser.storage.sync.set({ settings });
|
||||
showStatus("Settings saved successfully!", "success");
|
||||
} catch (error) {
|
||||
console.error("Error saving settings:", error);
|
||||
showStatus("Error saving settings", "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Load settings from storage or use defaults
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const data = await browser.storage.sync.get("settings");
|
||||
return { ...DEFAULT_SETTINGS, ...data.settings };
|
||||
} catch (error) {
|
||||
console.error("Error loading settings:", error);
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
}
|
||||
|
||||
// Show status message
|
||||
function showStatus(message, type) {
|
||||
const status = document.getElementById("status");
|
||||
status.textContent = message;
|
||||
status.className = type;
|
||||
status.style.display = "block";
|
||||
|
||||
setTimeout(() => {
|
||||
status.style.display = "none";
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
window.addEventListener("error", (event) => {
|
||||
console.error("Error:", event.error);
|
||||
showStatus("An error occurred", "error");
|
||||
});
|
Loading…
Reference in New Issue
Block a user