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