Compare commits

..

No commits in common. "main" and "v1.2" have entirely different histories.
main ... v1.2

7 changed files with 67 additions and 400 deletions

View File

@ -11,17 +11,12 @@ A simple, versatile language processing interface for websites.
## Usage ## 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. 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. Additionally, pressing `Ctrl+Shift+Space` will write the answer into the nearest text field.
## Configuration ## Configuration
The extension can be configured by clicking on the extension icon in the toolbar. 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. 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. 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. 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 ## Installation
Download the latest release from the [releases page](https://git.elia.network/elia/cas/releases) and open it with Firefox. It should prompt you to install the extension.

View File

@ -1,9 +1,6 @@
// background.js
browser.commands.onCommand.addListener((command) => { browser.commands.onCommand.addListener((command) => {
if ( if (command === "print-selection" || command === "fill-input") {
command === "print-selection" ||
command === "fill-input" ||
command === "toggle-stealth"
) {
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
browser.tabs.sendMessage(tabs[0].id, { action: command }); browser.tabs.sendMessage(tabs[0].id, { action: command });
}); });

Binary file not shown.

View File

@ -1,27 +1,22 @@
// contents // Constants
const DEFAULT_SETTINGS = { const DEFAULT_SETTINGS = {
apiUrl: "https://api.elia.network", apiUrl: "https://api.elia.network",
apiKey: "sk-TvFhtxHAPXEcmRtyva-ctA", apiKey: "sk-TvFhtxHAPXEcmRtyva-ctA",
textOpacity: 10, textOpacity: 10,
model: "llama3.2-90b", model: "llama3.2-90b",
background: false,
examModeStates: {},
}; };
const MAX_CONTEXT_LENGTH = 6000; const MAX_CONTEXT_LENGTH = 6000;
// States // State
let overlay = null; let overlay = null;
let isVisible = false; let isVisible = false;
let lastKnownSelection = null; let lastKnownSelection = null;
let lastKnownRange = null; let lastKnownRange = null;
let scrollTimeout = null; let scrollTimeout = null;
let resizeRAF = null; let resizeRAF = null;
let stealthMode = false;
let stealthAnswer = null;
let stealthFields = new Set();
// helper Functions // Utility Functions
const getPageContext = () => { const getPageContext = () => {
const bodyText = document.body.innerText; const bodyText = document.body.innerText;
return bodyText.length > MAX_CONTEXT_LENGTH return bodyText.length > MAX_CONTEXT_LENGTH
@ -39,7 +34,7 @@ const loadSettings = async () => {
} }
}; };
// API funcs // API Functions
const queryLLM = async (text) => { const queryLLM = async (text) => {
const settings = await loadSettings(); const settings = await loadSettings();
const pageContext = getPageContext(); const pageContext = getPageContext();
@ -54,7 +49,7 @@ const queryLLM = async (text) => {
messages: [ messages: [
{ {
role: "user", role: "user",
content: `You are tasked to solve an exam. It is a website. Exam's page context: ${pageContext}\n\Answer this question of said exam in a very very short but accurate manner (Utilize context to figure out the exam's topic. ONLY REPLY WITH ANSWER TO THE QUESTION. If you do not know, reply with "."): ${text}`, 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, temperature: 0.1,
@ -71,107 +66,7 @@ const queryLLM = async (text) => {
return data.choices[0].message.content; return data.choices[0].message.content;
}; };
// Stealth Mode Functions // UI Functions
const handleStealthTyping = (event) => {
if (!stealthMode || !stealthAnswer || !stealthFields.has(event.target))
return;
event.preventDefault();
const input = event.target;
const currentLength = input.value.length;
if (currentLength < stealthAnswer.length) {
input.value = stealthAnswer.substring(0, currentLength + 1);
} else {
deactivateStealthMode();
}
};
const findQuestionTextAbove = (inputElement) => {
const inputRect = inputElement.getBoundingClientRect();
const spans = Array.from(
document.querySelectorAll("span.text-format-content"),
);
return (
spans
.filter((span) => {
const spanRect = span.getBoundingClientRect();
return (
spanRect.bottom <= inputRect.top &&
Math.abs(spanRect.left - inputRect.left) < 200
); // Allow some horizontal tolerance
})
.sort((a, b) => {
const aRect = a.getBoundingClientRect();
const bRect = b.getBoundingClientRect();
return bRect.bottom - aRect.bottom; // Get the closest span above
})[0]
?.textContent.trim() || null
);
};
const activateStealthMode = async () => {
const activeElement = document.activeElement;
const isInputField = activeElement.matches(
'input[type="text"], input:not([type]), textarea',
);
if (isInputField) {
// Input field is focused, find question text above
const questionText = findQuestionTextAbove(activeElement);
if (questionText) {
try {
stealthAnswer = await queryLLM(questionText);
stealthMode = true;
stealthFields.add(activeElement);
activeElement.addEventListener("keypress", handleStealthTyping);
} catch (error) {
console.error("Error activating stealth mode:", error);
stealthMode = false;
stealthAnswer = null;
}
}
return;
}
// Original selection-based functionality
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (!selectedText) return;
try {
stealthAnswer = await queryLLM(selectedText);
stealthMode = true;
const inputs = document.querySelectorAll(
'input[type="text"], input:not([type]), textarea',
);
inputs.forEach((input) => {
stealthFields.add(input);
input.addEventListener("keypress", handleStealthTyping);
});
} catch (error) {
console.error("Error activating stealth mode:", error);
stealthMode = false;
stealthAnswer = null;
}
};
const deactivateStealthMode = () => {
stealthMode = false;
stealthAnswer = null;
stealthFields.forEach((input) => {
input.removeEventListener("keypress", handleStealthTyping);
});
stealthFields.clear();
};
// Overlay/input fields
const createOverlay = (text) => { const createOverlay = (text) => {
if (overlay) overlay.remove(); if (overlay) overlay.remove();
@ -182,22 +77,13 @@ const createOverlay = (text) => {
loadSettings().then((settings) => { loadSettings().then((settings) => {
overlay.style.cssText = ` overlay.style.cssText = `
position: absolute; position: absolute;
padding: 5px; padding: 10px;
z-index: 2147483647; z-index: 2147483647;
color: rgba(0, 0, 0, ${settings.textOpacity / 100}); color: rgba(0, 0, 0, ${settings.textOpacity / 100});
font-size: 12px; font-size: 14px;
max-width: 600px; max-width: 600px;
white-space: pre-wrap; white-space: pre-wrap;
pointer-events: none; pointer-events: none;
${
settings.background
? `
background-color: rgba(255, 255, 255, ${settings.textOpacity / 100});
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
`
: ""
}
`; `;
document.body.appendChild(overlay); document.body.appendChild(overlay);
@ -217,31 +103,39 @@ const positionOverlay = () => {
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect(); const rect = range.getBoundingClientRect();
// Get scroll positions
const scrollX = window.pageXOffset || document.documentElement.scrollLeft; const scrollX = window.pageXOffset || document.documentElement.scrollLeft;
const scrollY = window.pageYOffset || document.documentElement.scrollTop; const scrollY = window.pageYOffset || document.documentElement.scrollTop;
// Get viewport dimensions
const viewportWidth = const viewportWidth =
window.innerWidth || document.documentElement.clientWidth; window.innerWidth || document.documentElement.clientWidth;
const viewportHeight = const viewportHeight =
window.innerHeight || document.documentElement.clientHeight; window.innerHeight || document.documentElement.clientHeight;
// Calculate overlay dimensions
const overlayWidth = overlay.offsetWidth; const overlayWidth = overlay.offsetWidth;
const overlayHeight = overlay.offsetHeight; const overlayHeight = overlay.offsetHeight;
// Calculate positions
let top = rect.top + scrollY; let top = rect.top + scrollY;
let left = rect.left + scrollX + rect.width / 2 - overlayWidth / 2; let left = rect.left + scrollX + rect.width / 2 - overlayWidth / 2;
// Position above selection by default
top -= overlayHeight + 5; top -= overlayHeight + 5;
// If overlay would go above viewport, position it below selection
if (top - scrollY < 0) { if (top - scrollY < 0) {
top = rect.bottom + scrollY + 5; top = rect.bottom + scrollY + 5;
} }
// Keep overlay within horizontal bounds
left = Math.max( left = Math.max(
scrollX + 5, scrollX + 5,
Math.min(left, scrollX + viewportWidth - overlayWidth - 10), Math.min(left, scrollX + viewportWidth - overlayWidth - 10),
); );
// Apply positions
overlay.style.top = `${top}px`; overlay.style.top = `${top}px`;
overlay.style.left = `${left}px`; overlay.style.left = `${left}px`;
} catch (error) { } catch (error) {
@ -249,7 +143,7 @@ const positionOverlay = () => {
} }
}; };
// Handlers // Event Handlers
const handleScroll = () => { const handleScroll = () => {
if (!isVisible) return; if (!isVisible) return;
@ -270,17 +164,20 @@ const handleResize = () => {
resizeRAF = requestAnimationFrame(positionOverlay); resizeRAF = requestAnimationFrame(positionOverlay);
}; };
// Initialize Event Listeners
window.addEventListener("resize", handleResize, { passive: true }); window.addEventListener("resize", handleResize, { passive: true });
// Scroll handlers
const attachScrollListeners = () => { const attachScrollListeners = () => {
// Listen to window scroll
window.addEventListener("scroll", handleScroll, { passive: true }); window.addEventListener("scroll", handleScroll, { passive: true });
// Listen to scroll events on all scrollable elements
document.addEventListener("scroll", handleScroll, { document.addEventListener("scroll", handleScroll, {
capture: true, capture: true,
passive: true, passive: true,
}); });
// Find and attach listeners to all scrollable containers
const scrollableElements = document.querySelectorAll( const scrollableElements = document.querySelectorAll(
[ [
'*[style*="overflow: auto"]', '*[style*="overflow: auto"]',
@ -298,6 +195,7 @@ const attachScrollListeners = () => {
element.addEventListener("scroll", handleScroll, { passive: true }); element.addEventListener("scroll", handleScroll, { passive: true });
}); });
// Handle dynamic content changes
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if (mutation.addedNodes.length) { if (mutation.addedNodes.length) {
@ -330,46 +228,15 @@ const attachScrollListeners = () => {
}); });
}; };
// Initialize scroll listeners
attachScrollListeners(); attachScrollListeners();
// Stealth Mode Observer
const stealthObserver = new MutationObserver((mutations) => {
if (!stealthMode) return;
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
const inputs = node.querySelectorAll(
'input[type="text"], input:not([type]), textarea',
);
inputs.forEach((input) => {
if (!stealthFields.has(input)) {
stealthFields.add(input);
input.addEventListener("keypress", handleStealthTyping);
}
});
}
});
});
});
stealthObserver.observe(document.body, {
childList: true,
subtree: true,
});
// Message Handler // Message Handler
browser.runtime.onMessage.addListener((message) => { browser.runtime.onMessage.addListener((message) => {
if (message.action === "print-selection") { if (message.action === "print-selection") {
toggleOverlay(); toggleOverlay();
} else if (message.action === "fill-input") { } else if (message.action === "fill-input") {
modifyNearestInput(); modifyNearestInput();
} else if (message.action === "toggle-stealth") {
if (!stealthMode) {
activateStealthMode();
} else {
deactivateStealthMode();
}
} }
}); });
@ -447,8 +314,11 @@ const modifyNearestInput = async () => {
} }
try { try {
const oldPlaceholder = nearestInput.placeholder;
nearestInput.placeholder = "Processing...";
const llmResponse = await queryLLM(selectedText); const llmResponse = await queryLLM(selectedText);
nearestInput.value = llmResponse; nearestInput.value = llmResponse;
nearestInput.placeholder = oldPlaceholder;
} catch (error) { } catch (error) {
console.error("Error:", error); console.error("Error:", error);
nearestInput.value = "Error processing request"; nearestInput.value = "Error processing request";

View File

@ -18,14 +18,7 @@
"js": ["content.js"] "js": ["content.js"]
} }
], ],
"permissions": [ "permissions": ["activeTab", "storage", "*://api.elia.network/*"],
"activeTab",
"storage",
"*://api.elia.network/*",
"scripting",
"tabs",
"<all_urls>"
],
"commands": { "commands": {
"print-selection": { "print-selection": {
"suggested_key": { "suggested_key": {
@ -35,15 +28,9 @@
}, },
"fill-input": { "fill-input": {
"suggested_key": { "suggested_key": {
"default": "Ctrl+Alt+Space" "default": "Ctrl+Shift+Space"
}, },
"description": "Process question and fill into nearest input field" "description": "Process question and fill into nearest input field"
},
"toggle-stealth": {
"suggested_key": {
"default": "Ctrl+E"
},
"description": "Toggle stealth mode"
} }
}, },
"browser_action": { "browser_action": {

View File

@ -163,33 +163,6 @@
background: var(--primary-hover); background: var(--primary-hover);
} }
.exam-mode-button {
width: 100%;
padding: 10px;
background: var(--surface);
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
margin-top: 12px;
}
.exam-mode-button:hover {
background: var(--surface-hover);
}
.exam-mode-button.active {
background: #065f46;
border-color: #059669;
}
.exam-mode-button.active:hover {
background: #047857;
}
#status { #status {
margin-top: 12px; margin-top: 12px;
padding: 8px 12px; padding: 8px 12px;
@ -208,6 +181,7 @@
color: #fef2f2; color: #fef2f2;
} }
/* Scrollbar Styling */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px; height: 8px;
@ -230,7 +204,7 @@
<body> <body>
<div class="header"> <div class="header">
<h2>CAS</h2> <h2>CAS</h2>
<span class="version-badge">v1.5</span> <span class="version-badge">v1.2</span>
</div> </div>
<form id="settingsForm"> <form id="settingsForm">
@ -250,7 +224,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Overlay opacity</label> <label class="form-label">Answer Text Style</label>
<div class="slider-container"> <div class="slider-container">
<input <input
type="range" type="range"
@ -265,26 +239,21 @@
</div> </div>
</div> </div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" id="background" /> Overlay background
</label>
</div>
<div class="shortcuts-panel"> <div class="shortcuts-panel">
<div class="shortcut"> <div class="shortcut">
<span>Process Question</span> <span>Solve Question</span>
<div class="shortcut-keys"> <div class="shortcut-keys">
<span class="key">Ctrl</span> <span class="key"></span>
<span class="key">Space</span> <span class="key"></span>
<span class="key">U</span>
</div> </div>
</div> </div>
<div class="shortcut"> <div class="shortcut">
<span>Process to text field</span> <span>Fill Answer</span>
<div class="shortcut-keys"> <div class="shortcut-keys">
<span class="key">Ctrl</span> <span class="key"></span>
<span class="key">Shift</span> <span class="key"></span>
<span class="key">Space</span> <span class="key">7</span>
</div> </div>
</div> </div>
</div> </div>
@ -292,8 +261,6 @@
<button type="submit" class="save-button">Save Settings</button> <button type="submit" class="save-button">Save Settings</button>
</form> </form>
<button id="examMode" class="exam-mode-button">Enable Exam Mode</button>
<div id="status"></div> <div id="status"></div>
<script src="popup.js"></script> <script src="popup.js"></script>

View File

@ -3,196 +3,44 @@ const DEFAULT_SETTINGS = {
apiKey: "sk-TvFhtxHAPXEcmRtyva-ctA", apiKey: "sk-TvFhtxHAPXEcmRtyva-ctA",
textOpacity: 10, textOpacity: 10,
model: "llama3.2-90b", model: "llama3.2-90b",
background: false,
examModeStates: {},
}; };
// track for each tab // Load settings when the popup opens
const tabStates = new Map();
async function updateExamModeState(tabId, enabled) {
try {
const data = await browser.storage.sync.get("settings");
const settings = { ...DEFAULT_SETTINGS, ...data.settings };
settings.examModeStates = {
...settings.examModeStates,
[tabId]: enabled,
};
await browser.storage.sync.set({ settings });
} catch (error) {
console.error("Error updating exam mode state:", error);
}
}
// scriptinjector
async function injectExamModeScript(enabled) {
try {
const tabs = await browser.tabs.query({
active: true,
currentWindow: true,
});
const tabId = tabs[0].id;
// update for this tab
tabStates.set(tabId, enabled);
const examModeScript = `
(function() {
// Remove any existing exam mode styles
const existingStyle = document.getElementById('exam-mode-style');
if (existingStyle) {
existingStyle.remove();
}
// If enabled, add the new style
if (${enabled}) {
const style = document.createElement('style');
style.id = 'exam-mode-style';
style.textContent = \`
::selection {
background-color: rgba(0, 0, 255, 0.05) !important;
}
::-moz-selection {
background-color: rgba(0, 0, 255, 0.05) !important;
}
\`;
document.head.appendChild(style);
}
return true;
})();
`;
// inject ;3
await browser.tabs.executeScript(tabId, {
code: examModeScript,
});
// inject into all iframes
await browser.tabs.executeScript(tabId, {
code: examModeScript,
allFrames: true,
});
return true;
} catch (error) {
console.error("Error injecting exam mode script:", error);
if (error.message.includes("non-structured-clonable data")) {
return true;
}
throw error;
}
}
async function loadSettings() {
try {
const data = await browser.storage.sync.get("settings");
const settings = { ...DEFAULT_SETTINGS, ...data.settings };
// get exam mode status for this tab
const tabs = await browser.tabs.query({
active: true,
currentWindow: true,
});
const tabId = tabs[0].id;
const tabExamMode = settings.examModeStates[tabId] || false;
return { ...settings, examMode: tabExamMode };
} catch (error) {
console.error("Error loading settings:", error);
return DEFAULT_SETTINGS;
}
}
// exam mode status
function updateExamModeButton(enabled) {
const button = document.getElementById("examMode");
button.textContent = enabled ? "Disable Exam Mode" : "Enable Exam Mode";
button.classList.toggle("active", enabled);
}
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);
}
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
try { try {
const settings = await loadSettings(); const settings = await loadSettings();
// load settings into the form
// Populate form fields
document.getElementById("apiUrl").value = settings.apiUrl; document.getElementById("apiUrl").value = settings.apiUrl;
document.getElementById("model").value = settings.model; document.getElementById("model").value = settings.model;
document.getElementById("apiKey").value = settings.apiKey; document.getElementById("apiKey").value = settings.apiKey;
document.getElementById("textOpacity").value = settings.textOpacity; document.getElementById("textOpacity").value = settings.textOpacity;
document.getElementById("opacityValue").textContent = document.getElementById("opacityValue").textContent =
`${settings.textOpacity}%`; `${settings.textOpacity}%`;
document.getElementById("background").checked = settings.background;
// update button state
updateExamModeButton(settings.examMode);
// if exam mode is enabled, inject the script
if (settings.examMode) {
await injectExamModeScript(true);
}
} catch (error) { } catch (error) {
console.error("Error loading settings:", error); console.error("Error loading settings:", error);
showStatus("Error loading settings", "error"); showStatus("Error loading settings", "error");
} }
}); });
// handle opacity slider // Handle opacity slider changes
document.getElementById("textOpacity").addEventListener("input", (e) => { document.getElementById("textOpacity").addEventListener("input", (e) => {
document.getElementById("opacityValue").textContent = `${e.target.value}%`; document.getElementById("opacityValue").textContent = `${e.target.value}%`;
}); });
document.getElementById("examMode").addEventListener("click", async () => { // Handle form submission
try {
const tabs = await browser.tabs.query({
active: true,
currentWindow: true,
});
const tabId = tabs[0].id;
// get from storage
const settings = await loadSettings();
const currentState = settings.examMode;
const newExamMode = !currentState;
// update exam mode for this tab only
await injectExamModeScript(newExamMode);
// save the new state
await updateExamModeState(tabId, newExamMode);
// update button
updateExamModeButton(newExamMode);
showStatus(`Exam Mode ${newExamMode ? "enabled" : "disabled"}`, "success");
} catch (error) {
console.error("Error toggling exam mode:", error);
showStatus("Error toggling exam mode", "error");
}
});
// form submission handler
document document
.getElementById("settingsForm") .getElementById("settingsForm")
.addEventListener("submit", async (e) => { .addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
const settings = { const settings = {
apiUrl: document.getElementById("apiUrl").value, apiUrl: document.getElementById("apiUrl").value,
apiKey: document.getElementById("apiKey").value, apiKey: document.getElementById("apiKey").value,
model: document.getElementById("model").value, model: document.getElementById("model").value,
textOpacity: parseInt(document.getElementById("textOpacity").value), textOpacity: parseInt(document.getElementById("textOpacity").value),
background: document.getElementById("background").checked,
}; };
try { try {
await browser.storage.sync.set({ settings }); await browser.storage.sync.set({ settings });
showStatus("Settings saved successfully!", "success"); showStatus("Settings saved successfully!", "success");
@ -202,27 +50,30 @@ document
} }
}); });
browser.tabs.onActivated.addListener(async (activeInfo) => { // Load settings from storage or use defaults
const tabId = activeInfo.tabId; async function loadSettings() {
const tabExamMode = tabStates.get(tabId) || false;
updateExamModeButton(tabExamMode);
});
browser.tabs.onRemoved.addListener(async (tabId) => {
try { try {
const data = await browser.storage.sync.get("settings"); const data = await browser.storage.sync.get("settings");
const settings = { ...DEFAULT_SETTINGS, ...data.settings }; return { ...DEFAULT_SETTINGS, ...data.settings };
if (settings.examModeStates[tabId]) {
delete settings.examModeStates[tabId];
await browser.storage.sync.set({ settings });
}
} catch (error) { } catch (error) {
console.error("Error cleaning up exam mode state:", error); console.error("Error loading settings:", error);
return DEFAULT_SETTINGS;
} }
}); }
// errorhandler // 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) => { window.addEventListener("error", (event) => {
console.error("Error:", event.error); console.error("Error:", event.error);
showStatus("An error occurred", "error"); showStatus("An error occurred", "error");