diff --git a/extensions/chrome/LICENSE b/extensions/chrome/LICENSE new file mode 100644 index 0000000000..9431897fb6 --- /dev/null +++ b/extensions/chrome/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 DanswerAI, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/chrome/README.md b/extensions/chrome/README.md new file mode 100644 index 0000000000..c36d25d274 --- /dev/null +++ b/extensions/chrome/README.md @@ -0,0 +1,30 @@ +# Onyx Chrome Extension + +The Onyx chrome extension lets you research, create, and automate with LLMs powered by your team's unique knowledge. Just hit Ctrl + O on Mac or Alt + O on Windows to instantly access Onyx in your browser: + +πŸ’‘ Know what your company knows, instantly with the Onyx sidebar +πŸ’¬ Chat: Onyx provides a natural language chat interface as the main way of interacting with the features. +🌎 Internal Search: Ask questions and get answers from all your team's knowledge, powered by Onyx's 50+ connectors to all the tools your team uses +πŸš€ With a simple Ctrl + O on Mac or Alt + O on Windows - instantly summarize information from any work application + +⚑️ Get quick access to the work resources you need. +πŸ†• Onyx new tab page puts all of your company’s knowledge at your fingertips +πŸ€– Access custom AI Agents for unique use cases, and give them access to tools to take action. + +β€” + +Onyx connects with dozens of popular workplace apps like Google Drive, Jira, Confluence, Slack, and more. Use this extension if you have an account created by your team admin. + +## Installation + +For Onyx Cloud Users, please visit the Chrome Plugin Store (pending approval still) + +## Development + +- Load unpacked extension in your browser +- Modify files in `src` directory +- Refresh extension in Chrome + +## Contributing + +Submit issues or pull requests for improvements diff --git a/extensions/chrome/manifest.json b/extensions/chrome/manifest.json new file mode 100644 index 0000000000..b876cea617 --- /dev/null +++ b/extensions/chrome/manifest.json @@ -0,0 +1,70 @@ +{ + "manifest_version": 3, + "name": "Onyx", + "version": "1.0", + "description": "Onyx lets you research, create, and automate with LLMs powered by your team's unique knowledge", + "permissions": [ + "sidePanel", + "storage", + "activeTab", + "tabs" + ], + "host_permissions": [""], + "background": { + "service_worker": "service_worker.js", + "type": "module" + }, + "action": { + "default_icon": { + "16": "public/icon16.png", + "48": "public/icon48.png", + "128": "public/icon128.png" + }, + "default_popup": "src/pages/popup.html" + }, + "icons": { + "16": "public/icon16.png", + "48": "public/icon48.png", + "128": "public/icon128.png" + }, + "options_page": "src/pages/options.html", + "chrome_url_overrides": { + "newtab": "src/pages/onyx_home.html" + }, + "commands": { + "toggleNewTabOverride": { + "suggested_key": { + "default": "Ctrl+Shift+O", + "mac": "Command+Shift+O" + }, + "description": "Toggle Onyx New Tab Override" + }, + "openSidePanel": { + "suggested_key": { + "default": "Ctrl+O", + "windows": "Alt+O", + "mac": "MacCtrl+O" + }, + "description": "Open Onyx Side Panel" + } + }, + "side_panel": { + "default_path": "src/pages/panel.html" + }, + "omnibox": { + "keyword": "onyx" + }, + "content_scripts": [ + { + "matches": [""], + "js": ["src/utils/selection-icon.js"], + "css": ["src/styles/selection-icon.css"] + } + ], + "web_accessible_resources": [ + { + "resources": ["public/icon32.png"], + "matches": [""] + } + ] +} diff --git a/extensions/chrome/public/icon128.png b/extensions/chrome/public/icon128.png new file mode 100644 index 0000000000..a786b25e82 Binary files /dev/null and b/extensions/chrome/public/icon128.png differ diff --git a/extensions/chrome/public/icon16.png b/extensions/chrome/public/icon16.png new file mode 100644 index 0000000000..9c0c9929df Binary files /dev/null and b/extensions/chrome/public/icon16.png differ diff --git a/extensions/chrome/public/icon32.png b/extensions/chrome/public/icon32.png new file mode 100644 index 0000000000..bceb4272f8 Binary files /dev/null and b/extensions/chrome/public/icon32.png differ diff --git a/extensions/chrome/public/icon48.png b/extensions/chrome/public/icon48.png new file mode 100644 index 0000000000..752f3f54ae Binary files /dev/null and b/extensions/chrome/public/icon48.png differ diff --git a/extensions/chrome/public/logo.png b/extensions/chrome/public/logo.png new file mode 100644 index 0000000000..0a5aecd7c8 Binary files /dev/null and b/extensions/chrome/public/logo.png differ diff --git a/extensions/chrome/service_worker.js b/extensions/chrome/service_worker.js new file mode 100644 index 0000000000..250ef5629c --- /dev/null +++ b/extensions/chrome/service_worker.js @@ -0,0 +1,276 @@ +import { + DEFAULT_ONYX_DOMAIN, + CHROME_SPECIFIC_STORAGE_KEYS, + ACTIONS, + SIDE_PANEL_PATH, +} from "./src/utils/constants.js"; + +// Track side panel state per window +const sidePanelOpenState = new Map(); + +// Open welcome page on first install +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason === "install") { + chrome.storage.local.get( + { [CHROME_SPECIFIC_STORAGE_KEYS.ONBOARDING_COMPLETE]: false }, + (result) => { + if (!result[CHROME_SPECIFIC_STORAGE_KEYS.ONBOARDING_COMPLETE]) { + chrome.tabs.create({ url: "src/pages/welcome.html" }); + } + }, + ); + } +}); + +async function setupSidePanel() { + if (chrome.sidePanel) { + try { + // Don't auto-open side panel on action click since we have a popup menu + await chrome.sidePanel.setPanelBehavior({ + openPanelOnActionClick: false, + }); + } catch (error) { + console.error("Error setting up side panel:", error); + } + } +} + +async function openSidePanel(tabId) { + try { + await chrome.sidePanel.open({ tabId }); + } catch (error) { + console.error("Error opening side panel:", error); + } +} + +async function sendToOnyx(info, tab) { + const selectedText = encodeURIComponent(info.selectionText); + const currentUrl = encodeURIComponent(tab.url); + + try { + const result = await chrome.storage.local.get({ + [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN, + }); + const url = `${ + result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] + }${SIDE_PANEL_PATH}?user-prompt=${selectedText}`; + + await openSidePanel(tab.id); + chrome.runtime.sendMessage({ + action: ACTIONS.OPEN_SIDE_PANEL_WITH_INPUT, + url: url, + pageUrl: tab.url, + }); + } catch (error) { + console.error("Error sending to Onyx:", error); + } +} + +async function toggleNewTabOverride() { + try { + const result = await chrome.storage.local.get( + CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB, + ); + const newValue = + !result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; + await chrome.storage.local.set({ + [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: newValue, + }); + + chrome.notifications.create({ + type: "basic", + iconUrl: "icon.png", + title: "Onyx New Tab", + message: `New Tab Override ${newValue ? "enabled" : "disabled"}`, + }); + + // Send a message to inform all tabs about the change + chrome.tabs.query({}, (tabs) => { + tabs.forEach((tab) => { + chrome.tabs.sendMessage(tab.id, { + action: "newTabOverrideToggled", + value: newValue, + }); + }); + }); + } catch (error) { + console.error("Error toggling new tab override:", error); + } +} + +// Note: This listener won't fire when a popup is defined in manifest.json +// The popup will show instead. This is kept as a fallback if popup is removed. +chrome.action.onClicked.addListener((tab) => { + openSidePanel(tab.id); +}); + +chrome.commands.onCommand.addListener(async (command) => { + if (command === ACTIONS.SEND_TO_ONYX) { + try { + const [tab] = await chrome.tabs.query({ + active: true, + lastFocusedWindow: true, + }); + if (tab) { + const response = await chrome.tabs.sendMessage(tab.id, { + action: ACTIONS.GET_SELECTED_TEXT, + }); + const selectedText = response?.selectedText || ""; + sendToOnyx({ selectionText: selectedText }, tab); + } + } catch (error) { + console.error("Error sending to Onyx:", error); + } + } else if (command === ACTIONS.TOGGLE_NEW_TAB_OVERRIDE) { + toggleNewTabOverride(); + } else if (command === ACTIONS.CLOSE_SIDE_PANEL) { + try { + await chrome.sidePanel.hide(); + } catch (error) { + console.error("Error closing side panel via command:", error); + } + } else if (command === ACTIONS.OPEN_SIDE_PANEL) { + chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { + if (tabs && tabs.length > 0) { + const tab = tabs[0]; + const windowId = tab.windowId; + const isOpen = sidePanelOpenState.get(windowId) || false; + + if (isOpen) { + chrome.sidePanel.setOptions({ enabled: false }, () => { + chrome.sidePanel.setOptions({ enabled: true }); + sidePanelOpenState.set(windowId, false); + }); + } else { + chrome.sidePanel.open({ tabId: tab.id }); + sidePanelOpenState.set(windowId, true); + } + } + }); + return; + } else { + console.log("Unhandled command:", command); + } +}); + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === ACTIONS.GET_CURRENT_ONYX_DOMAIN) { + chrome.storage.local.get( + { [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN }, + (result) => { + sendResponse({ + [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: + result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN], + }); + }, + ); + return true; + } + if (request.action === ACTIONS.CLOSE_SIDE_PANEL) { + closeSidePanel(); + chrome.storage.local.get( + { [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN }, + (result) => { + chrome.tabs.create({ + url: `${result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]}/auth/login`, + active: true, + }); + }, + ); + return true; + } + if (request.action === ACTIONS.OPEN_SIDE_PANEL_WITH_INPUT) { + const { selectedText, pageUrl } = request; + const tabId = sender.tab?.id; + const windowId = sender.tab?.windowId; + + if (tabId && windowId) { + chrome.storage.local.get( + { [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN }, + (result) => { + const encodedText = encodeURIComponent(selectedText); + const onyxDomain = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; + const url = `${onyxDomain}${SIDE_PANEL_PATH}?user-prompt=${encodedText}`; + + chrome.storage.session.set({ + pendingInput: { + url: url, + pageUrl: pageUrl, + timestamp: Date.now(), + }, + }); + + chrome.sidePanel + .open({ windowId }) + .then(() => { + chrome.runtime.sendMessage({ + action: ACTIONS.OPEN_ONYX_WITH_INPUT, + url: url, + pageUrl: pageUrl, + }); + }) + .catch((error) => { + console.error( + "[Onyx SW] Error opening side panel with text:", + error, + ); + }); + }, + ); + } else { + console.error("[Onyx SW] Missing tabId or windowId"); + } + return true; + } +}); + +chrome.storage.onChanged.addListener((changes, namespace) => { + if ( + namespace === "local" && + changes[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB] + ) { + const newValue = + changes[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB] + .newValue; + + if (newValue === false) { + chrome.runtime.openOptionsPage(); + } + } +}); + +chrome.windows.onRemoved.addListener((windowId) => { + sidePanelOpenState.delete(windowId); +}); + +chrome.omnibox.setDefaultSuggestion({ + description: 'Search Onyx for "%s"', +}); + +chrome.omnibox.onInputEntered.addListener(async (text) => { + try { + const result = await chrome.storage.local.get({ + [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN, + }); + + const domain = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; + const searchUrl = `${domain}/chat?user-prompt=${encodeURIComponent(text)}`; + + chrome.tabs.update({ url: searchUrl }); + } catch (error) { + console.error("Error handling omnibox search:", error); + } +}); + +chrome.omnibox.onInputChanged.addListener((text, suggest) => { + if (text.trim()) { + suggest([ + { + content: text, + description: `Search Onyx for "${text}"`, + }, + ]); + } +}); + +setupSidePanel(); diff --git a/extensions/chrome/src/.DS_Store b/extensions/chrome/src/.DS_Store new file mode 100644 index 0000000000..51707bc4ea Binary files /dev/null and b/extensions/chrome/src/.DS_Store differ diff --git a/extensions/chrome/src/pages/onyx_home.html b/extensions/chrome/src/pages/onyx_home.html new file mode 100644 index 0000000000..2ec8984c9b --- /dev/null +++ b/extensions/chrome/src/pages/onyx_home.html @@ -0,0 +1,76 @@ + + + + + + + Onyx Home + + + + + +
+
+ +
+ + + diff --git a/extensions/chrome/src/pages/onyx_home.js b/extensions/chrome/src/pages/onyx_home.js new file mode 100644 index 0000000000..3814bd8f5c --- /dev/null +++ b/extensions/chrome/src/pages/onyx_home.js @@ -0,0 +1,248 @@ +import { + CHROME_MESSAGE, + CHROME_SPECIFIC_STORAGE_KEYS, + WEB_MESSAGE, +} from "../utils/constants.js"; +import { + showErrorModal, + hideErrorModal, + initErrorModal, +} from "../utils/error-modal.js"; +import { getOnyxDomain } from "../utils/storage.js"; + +(function () { + let mainIframe = document.getElementById("onyx-iframe"); + let preloadedIframe = null; + const background = document.getElementById("background"); + const content = document.getElementById("content"); + const DEFAULT_LIGHT_BACKGROUND_IMAGE = + "https://images.unsplash.com/photo-1692520883599-d543cfe6d43d?q=80&w=2666&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"; + const DEFAULT_DARK_BACKGROUND_IMAGE = + "https://images.unsplash.com/photo-1692520883599-d543cfe6d43d?q=80&w=2666&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"; + + let iframeLoadTimeout; + let iframeLoaded = false; + + initErrorModal(); + + async function preloadChatInterface() { + preloadedIframe = document.createElement("iframe"); + + const domain = await getOnyxDomain(); + preloadedIframe.src = domain + "/chat"; + preloadedIframe.style.opacity = "0"; + preloadedIframe.style.visibility = "hidden"; + preloadedIframe.style.transition = "opacity 0.3s ease-in"; + preloadedIframe.style.border = "none"; + preloadedIframe.style.width = "100%"; + preloadedIframe.style.height = "100%"; + preloadedIframe.style.position = "absolute"; + preloadedIframe.style.top = "0"; + preloadedIframe.style.left = "0"; + preloadedIframe.style.zIndex = "1"; + content.appendChild(preloadedIframe); + } + + function setIframeSrc(url) { + mainIframe.src = url; + startIframeLoadTimeout(); + iframeLoaded = false; + } + + function startIframeLoadTimeout() { + clearTimeout(iframeLoadTimeout); + iframeLoadTimeout = setTimeout(() => { + if (!iframeLoaded) { + try { + if ( + mainIframe.contentWindow.location.pathname.includes("/auth/login") + ) { + showLoginPage(); + } else { + showErrorModal(mainIframe.src); + } + } catch (error) { + showErrorModal(mainIframe.src); + } + } + }, 2500); + } + + function showLoginPage() { + background.style.opacity = "0"; + mainIframe.style.opacity = "1"; + mainIframe.style.visibility = "visible"; + content.style.opacity = "1"; + hideErrorModal(); + } + + function setTheme(theme, customBackgroundImage) { + const imageUrl = + customBackgroundImage || + (theme === "dark" + ? DEFAULT_DARK_BACKGROUND_IMAGE + : DEFAULT_LIGHT_BACKGROUND_IMAGE); + background.style.backgroundImage = `url('${imageUrl}')`; + } + + function fadeInContent() { + content.style.transition = "opacity 0.5s ease-in"; + mainIframe.style.transition = "opacity 0.5s ease-in"; + content.style.opacity = "0"; + mainIframe.style.opacity = "0"; + mainIframe.style.visibility = "visible"; + + requestAnimationFrame(() => { + content.style.opacity = "1"; + mainIframe.style.opacity = "1"; + + setTimeout(() => { + background.style.transition = "opacity 0.3s ease-out"; + background.style.opacity = "0"; + }, 500); + }); + } + + function checkOnyxPreference() { + chrome.storage.local.get( + [ + CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB, + CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN, + ], + (items) => { + let useOnyxAsDefaultNewTab = + items[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; + + if (useOnyxAsDefaultNewTab === undefined) { + useOnyxAsDefaultNewTab = !!( + localStorage.getItem( + CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB, + ) === "1" + ); + chrome.storage.local.set({ + [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: + useOnyxAsDefaultNewTab, + }); + } + + if (!useOnyxAsDefaultNewTab) { + chrome.tabs.update({ + url: "chrome://new-tab-page", + }); + return; + } + + setIframeSrc( + items[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] + "/chat/nrf", + ); + }, + ); + } + + function loadThemeAndBackground() { + chrome.storage.local.get( + [ + CHROME_SPECIFIC_STORAGE_KEYS.THEME, + CHROME_SPECIFIC_STORAGE_KEYS.BACKGROUND_IMAGE, + CHROME_SPECIFIC_STORAGE_KEYS.DARK_BG_URL, + CHROME_SPECIFIC_STORAGE_KEYS.LIGHT_BG_URL, + ], + function (result) { + const theme = result[CHROME_SPECIFIC_STORAGE_KEYS.THEME] || "light"; + const customBackgroundImage = + result[CHROME_SPECIFIC_STORAGE_KEYS.BACKGROUND_IMAGE]; + const darkBgUrl = result[CHROME_SPECIFIC_STORAGE_KEYS.DARK_BG_URL]; + const lightBgUrl = result[CHROME_SPECIFIC_STORAGE_KEYS.LIGHT_BG_URL]; + + let backgroundImage; + if (customBackgroundImage) { + backgroundImage = customBackgroundImage; + } else if (theme === "dark" && darkBgUrl) { + backgroundImage = darkBgUrl; + } else if (theme === "light" && lightBgUrl) { + backgroundImage = lightBgUrl; + } + + setTheme(theme, backgroundImage); + checkOnyxPreference(); + }, + ); + } + + function loadNewPage(newSrc) { + if (preloadedIframe && preloadedIframe.contentWindow) { + preloadedIframe.contentWindow.postMessage( + { type: WEB_MESSAGE.PAGE_CHANGE, href: newSrc }, + "*", + ); + } else { + console.error("Preloaded iframe not available"); + } + } + + function completePendingPageLoad() { + if (preloadedIframe) { + preloadedIframe.style.visibility = "visible"; + preloadedIframe.style.opacity = "1"; + preloadedIframe.style.zIndex = "1"; + mainIframe.style.zIndex = "2"; + mainIframe.style.opacity = "0"; + + setTimeout(() => { + if (content.contains(mainIframe)) { + content.removeChild(mainIframe); + } + + mainIframe = preloadedIframe; + mainIframe.id = "onyx-iframe"; + mainIframe.style.zIndex = ""; + iframeLoaded = true; + clearTimeout(iframeLoadTimeout); + }, 200); + } else { + console.warn("No preloaded iframe available"); + } + } + + chrome.storage.onChanged.addListener(function (changes, namespace) { + if (namespace === "local" && changes.useOnyxAsDefaultNewTab) { + checkOnyxPreference(); + } + }); + + window.addEventListener("message", function (event) { + if (event.data.type === CHROME_MESSAGE.SET_DEFAULT_NEW_TAB) { + chrome.storage.local.set({ useOnyxAsDefaultNewTab: event.data.value }); + } else if (event.data.type === CHROME_MESSAGE.ONYX_APP_LOADED) { + clearTimeout(iframeLoadTimeout); + hideErrorModal(); + fadeInContent(); + iframeLoaded = true; + } else if (event.data.type === CHROME_MESSAGE.PREFERENCES_UPDATED) { + const { theme, backgroundUrl } = event.data.payload; + chrome.storage.local.set( + { + [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: theme, + [CHROME_SPECIFIC_STORAGE_KEYS.BACKGROUND_IMAGE]: backgroundUrl, + }, + () => {}, + ); + } else if (event.data.type === CHROME_MESSAGE.LOAD_NEW_PAGE) { + loadNewPage(event.data.href); + } else if (event.data.type === CHROME_MESSAGE.LOAD_NEW_CHAT_PAGE) { + completePendingPageLoad(); + } + }); + + mainIframe.onload = function () { + clearTimeout(iframeLoadTimeout); + startIframeLoadTimeout(); + }; + + mainIframe.onerror = function (error) { + showErrorModal(mainIframe.src); + }; + + loadThemeAndBackground(); + preloadChatInterface(); +})(); diff --git a/extensions/chrome/src/pages/options.html b/extensions/chrome/src/pages/options.html new file mode 100644 index 0000000000..754aff17e6 --- /dev/null +++ b/extensions/chrome/src/pages/options.html @@ -0,0 +1,515 @@ + + + + + + + Onyx - Settings + + + + + +
+
+
+
+
+ Onyx +
+

Settings

+
+ +
+ +
+ +
+
General
+
+
+
+ +
+ The root URL for your Onyx instance +
+
+
+
+
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
Search Engine
+
+
+
+ +
+ Type onyx followed by a space in Chrome's address + bar, then enter your search query and press Enter +
+
+
+
+
+
+
+ Searches will be directed to your configured Onyx instance + at the Root Domain above +
+
+
+
+
+ + +
+

+ +
+
+
+
+ + + diff --git a/extensions/chrome/src/pages/options.js b/extensions/chrome/src/pages/options.js new file mode 100644 index 0000000000..951d01c197 --- /dev/null +++ b/extensions/chrome/src/pages/options.js @@ -0,0 +1,142 @@ +import { + CHROME_SPECIFIC_STORAGE_KEYS, + DEFAULT_ONYX_DOMAIN, +} from "../utils/constants.js"; + +document.addEventListener("DOMContentLoaded", function () { + const domainInput = document.getElementById("onyxDomain"); + const useOnyxAsDefaultToggle = document.getElementById("useOnyxAsDefault"); + const statusContainer = document.getElementById("statusContainer"); + const statusElement = document.getElementById("status"); + const newTabButton = document.getElementById("newTab"); + const themeToggle = document.getElementById("themeToggle"); + const themeIcon = document.getElementById("themeIcon"); + + let currentTheme = "dark"; + + function updateThemeIcon(theme) { + if (!themeIcon) return; + + if (theme === "light") { + themeIcon.innerHTML = ` + + + `; + } else { + themeIcon.innerHTML = ` + + `; + } + } + + function loadStoredValues() { + chrome.storage.local.get( + { + [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN, + [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: false, + [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: "dark", + }, + (result) => { + if (domainInput) + domainInput.value = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; + if (useOnyxAsDefaultToggle) + useOnyxAsDefaultToggle.checked = + result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; + + currentTheme = result[CHROME_SPECIFIC_STORAGE_KEYS.THEME] || "dark"; + updateThemeIcon(currentTheme); + + document.body.className = currentTheme === "light" ? "light-theme" : ""; + }, + ); + } + + function saveSettings() { + const domain = domainInput.value.trim(); + const useOnyxAsDefault = useOnyxAsDefaultToggle + ? useOnyxAsDefaultToggle.checked + : false; + + chrome.storage.local.set( + { + [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: domain, + [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: + useOnyxAsDefault, + [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: currentTheme, + }, + () => { + showStatusMessage( + useOnyxAsDefault + ? "Settings updated. Open a new tab to test it out. Click on the extension icon to bring up Onyx from any page." + : "Settings updated.", + ); + }, + ); + } + + function showStatusMessage(message) { + if (statusElement) { + const useOnyxAsDefault = useOnyxAsDefaultToggle + ? useOnyxAsDefaultToggle.checked + : false; + + statusElement.textContent = + message || + (useOnyxAsDefault + ? "Settings updated. Open a new tab to test it out. Click on the extension icon to bring up Onyx from any page." + : "Settings updated."); + + if (newTabButton) { + newTabButton.style.display = useOnyxAsDefault ? "block" : "none"; + } + } + + if (statusContainer) { + statusContainer.classList.add("show"); + } + + setTimeout(hideStatusMessage, 5000); + } + + function hideStatusMessage() { + if (statusContainer) { + statusContainer.classList.remove("show"); + } + } + + function toggleTheme() { + currentTheme = currentTheme === "light" ? "dark" : "light"; + updateThemeIcon(currentTheme); + + document.body.className = currentTheme === "light" ? "light-theme" : ""; + + chrome.storage.local.set({ + [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: currentTheme, + }); + } + + function openNewTab() { + chrome.tabs.create({}); + } + + if (domainInput) { + domainInput.addEventListener("input", () => { + clearTimeout(domainInput.saveTimeout); + domainInput.saveTimeout = setTimeout(saveSettings, 1000); + }); + } + + if (useOnyxAsDefaultToggle) { + useOnyxAsDefaultToggle.addEventListener("change", saveSettings); + } + + if (themeToggle) { + themeToggle.addEventListener("click", toggleTheme); + } + + if (newTabButton) { + newTabButton.addEventListener("click", openNewTab); + } + + loadStoredValues(); +}); diff --git a/extensions/chrome/src/pages/panel.html b/extensions/chrome/src/pages/panel.html new file mode 100644 index 0000000000..b15d98e134 --- /dev/null +++ b/extensions/chrome/src/pages/panel.html @@ -0,0 +1,91 @@ + + + + + + + Onyx Panel + + + + + +
+ +
Loading Onyx...
+
+ + + + + diff --git a/extensions/chrome/src/pages/panel.js b/extensions/chrome/src/pages/panel.js new file mode 100644 index 0000000000..bf0e1e5bcf --- /dev/null +++ b/extensions/chrome/src/pages/panel.js @@ -0,0 +1,127 @@ +import { showErrorModal, showAuthModal } from "../utils/error-modal.js"; +import { + ACTIONS, + CHROME_MESSAGE, + WEB_MESSAGE, + CHROME_SPECIFIC_STORAGE_KEYS, + SIDE_PANEL_PATH, +} from "../utils/constants.js"; +(function () { + const iframe = document.getElementById("onyx-panel-iframe"); + const loadingScreen = document.getElementById("loading-screen"); + + let currentUrl = ""; + let iframeLoaded = false; + let iframeLoadTimeout; + let authRequired = false; + + async function checkPendingInput() { + try { + const result = await chrome.storage.session.get("pendingInput"); + if (result.pendingInput) { + const { url, pageUrl, timestamp } = result.pendingInput; + if (Date.now() - timestamp < 5000) { + setIframeSrc(url, pageUrl); + await chrome.storage.session.remove("pendingInput"); + return true; + } + await chrome.storage.session.remove("pendingInput"); + } + } catch (error) { + console.error("[Onyx Panel] Error checking pending input:", error); + } + return false; + } + + async function initializePanel() { + loadingScreen.style.display = "flex"; + loadingScreen.style.opacity = "1"; + iframe.style.opacity = "0"; + + // Check for pending input first (from selection icon click) + const hasPendingInput = await checkPendingInput(); + if (!hasPendingInput) { + loadOnyxDomain(); + } + } + + function setIframeSrc(url, pageUrl) { + iframe.src = url; + currentUrl = pageUrl; + } + + function sendWebsiteToIframe(pageUrl) { + if (iframe.contentWindow && pageUrl !== currentUrl) { + iframe.contentWindow.postMessage( + { + type: WEB_MESSAGE.PAGE_CHANGE, + url: pageUrl, + }, + "*", + ); + currentUrl = pageUrl; + } + } + + function startIframeLoadTimeout() { + iframeLoadTimeout = setTimeout(() => { + if (!iframeLoaded) { + if (authRequired) { + showAuthModal(); + } else { + showErrorModal(iframe.src); + } + } + }, 2500); + } + + function handleMessage(event) { + if (event.data.type === CHROME_MESSAGE.ONYX_APP_LOADED) { + clearTimeout(iframeLoadTimeout); + iframeLoaded = true; + showIframe(); + if (iframe.contentWindow) { + iframe.contentWindow.postMessage({ type: "PANEL_READY" }, "*"); + } + } else if (event.data.type === CHROME_MESSAGE.AUTH_REQUIRED) { + authRequired = true; + } + } + + function showIframe() { + iframe.style.opacity = "1"; + loadingScreen.style.opacity = "0"; + setTimeout(() => { + loadingScreen.style.display = "none"; + }, 500); + } + + async function loadOnyxDomain() { + const response = await chrome.runtime.sendMessage({ + action: ACTIONS.GET_CURRENT_ONYX_DOMAIN, + }); + if (response && response[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]) { + setIframeSrc( + response[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] + SIDE_PANEL_PATH, + "", + ); + } else { + console.warn("Onyx domain not found, using default"); + const domain = await getOnyxDomain(); + setIframeSrc(domain + SIDE_PANEL_PATH, ""); + } + } + + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === ACTIONS.OPEN_ONYX_WITH_INPUT) { + setIframeSrc(request.url, request.pageUrl); + } else if (request.action === ACTIONS.UPDATE_PAGE_URL) { + sendWebsiteToIframe(request.pageUrl); + } + }); + + window.addEventListener("message", handleMessage); + + initializePanel(); + startIframeLoadTimeout(); +})(); diff --git a/extensions/chrome/src/pages/popup.html b/extensions/chrome/src/pages/popup.html new file mode 100644 index 0000000000..78fe01ed12 --- /dev/null +++ b/extensions/chrome/src/pages/popup.html @@ -0,0 +1,252 @@ + + + + + + + Onyx + + + + + + + + diff --git a/extensions/chrome/src/pages/popup.js b/extensions/chrome/src/pages/popup.js new file mode 100644 index 0000000000..e31f2ec9c1 --- /dev/null +++ b/extensions/chrome/src/pages/popup.js @@ -0,0 +1,58 @@ +import { CHROME_SPECIFIC_STORAGE_KEYS } from "../utils/constants.js"; + +document.addEventListener("DOMContentLoaded", async function () { + const defaultNewTabToggle = document.getElementById("defaultNewTabToggle"); + const openSidePanelButton = document.getElementById("openSidePanel"); + const openOptionsButton = document.getElementById("openOptions"); + + async function loadSetting() { + const result = await chrome.storage.local.get({ + [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: false, + }); + if (defaultNewTabToggle) { + defaultNewTabToggle.checked = + result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; + } + } + + async function toggleSetting() { + const currentValue = defaultNewTabToggle.checked; + await chrome.storage.local.set({ + [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: currentValue, + }); + } + + async function openSidePanel() { + try { + const [tab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + if (tab && chrome.sidePanel) { + await chrome.sidePanel.open({ tabId: tab.id }); + window.close(); + } + } catch (error) { + console.error("Error opening side panel:", error); + } + } + + function openOptions() { + chrome.runtime.openOptionsPage(); + window.close(); + } + + await loadSetting(); + + if (defaultNewTabToggle) { + defaultNewTabToggle.addEventListener("change", toggleSetting); + } + + if (openSidePanelButton) { + openSidePanelButton.addEventListener("click", openSidePanel); + } + + if (openOptionsButton) { + openOptionsButton.addEventListener("click", openOptions); + } +}); diff --git a/extensions/chrome/src/pages/welcome.html b/extensions/chrome/src/pages/welcome.html new file mode 100644 index 0000000000..961f6bb508 --- /dev/null +++ b/extensions/chrome/src/pages/welcome.html @@ -0,0 +1,618 @@ + + + + + + Welcome to Onyx + + + + + + + + +
+
+
+
+
+ Onyx +
+

Onyx

+
+ +
+ +
+
+
+
+
+ + +
+

Welcome to Onyx

+

+ Enter your Onyx instance URL to get started. This is where your + Onyx deployment is hosted. +

+ +
+ + +
+ +
+ +
+
+ + +
+

Customize Your Experience

+

+ Set Onyx as your new tab page for quick access to your AI + assistant. +

+ +
+
+
+ Use Onyx as new tab page + Open Onyx every time you create a new tab +
+ +
+
+ +
+ + +
+
+
+
+
+ + + diff --git a/extensions/chrome/src/pages/welcome.js b/extensions/chrome/src/pages/welcome.js new file mode 100644 index 0000000000..c4e3be96d2 --- /dev/null +++ b/extensions/chrome/src/pages/welcome.js @@ -0,0 +1,192 @@ +import { + CHROME_SPECIFIC_STORAGE_KEYS, + DEFAULT_ONYX_DOMAIN, +} from "../utils/constants.js"; + +document.addEventListener("DOMContentLoaded", function () { + const domainInput = document.getElementById("onyxDomain"); + const useOnyxAsDefaultToggle = document.getElementById("useOnyxAsDefault"); + const continueBtn = document.getElementById("continueBtn"); + const backBtn = document.getElementById("backBtn"); + const finishBtn = document.getElementById("finishBtn"); + const themeToggle = document.getElementById("themeToggle"); + const themeIcon = document.getElementById("themeIcon"); + + const step1 = document.getElementById("step1"); + const step2 = document.getElementById("step2"); + const stepDots = document.querySelectorAll(".step-dot"); + + let currentStep = 1; + let currentTheme = "dark"; + + // Initialize theme based on system preference or stored value + function initTheme() { + chrome.storage.local.get( + { [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: null }, + (result) => { + const storedTheme = result[CHROME_SPECIFIC_STORAGE_KEYS.THEME]; + if (storedTheme) { + currentTheme = storedTheme; + } else { + // Check system preference + currentTheme = window.matchMedia("(prefers-color-scheme: light)") + .matches + ? "light" + : "dark"; + } + applyTheme(); + }, + ); + } + + function applyTheme() { + document.body.className = currentTheme === "light" ? "light-theme" : ""; + updateThemeIcon(); + } + + function updateThemeIcon() { + if (!themeIcon) return; + + if (currentTheme === "light") { + themeIcon.innerHTML = ` + + + `; + } else { + themeIcon.innerHTML = ` + + `; + } + } + + function toggleTheme() { + currentTheme = currentTheme === "light" ? "dark" : "light"; + applyTheme(); + chrome.storage.local.set({ + [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: currentTheme, + }); + } + + function goToStep(step) { + if (step === 1) { + step2.classList.remove("active"); + setTimeout(() => { + step1.classList.add("active"); + }, 50); + } else if (step === 2) { + step1.classList.remove("active"); + setTimeout(() => { + step2.classList.add("active"); + }, 50); + } + + stepDots.forEach((dot) => { + const dotStep = parseInt(dot.dataset.step); + if (dotStep === step) { + dot.classList.add("active"); + } else { + dot.classList.remove("active"); + } + }); + + currentStep = step; + } + + // Validate domain input + function validateDomain(domain) { + if (!domain) return false; + try { + new URL(domain); + return true; + } catch { + return false; + } + } + + function handleContinue() { + const domain = domainInput.value.trim(); + + if (domain && !validateDomain(domain)) { + domainInput.style.borderColor = "rgba(255, 100, 100, 0.5)"; + domainInput.focus(); + return; + } + + domainInput.style.borderColor = ""; + goToStep(2); + } + + function handleBack() { + goToStep(1); + } + + function handleFinish() { + const domain = domainInput.value.trim() || DEFAULT_ONYX_DOMAIN; + const useOnyxAsDefault = useOnyxAsDefaultToggle.checked; + + chrome.storage.local.set( + { + [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: domain, + [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: + useOnyxAsDefault, + [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: currentTheme, + [CHROME_SPECIFIC_STORAGE_KEYS.ONBOARDING_COMPLETE]: true, + }, + () => { + // Open a new tab if they enabled the new tab feature, otherwise just close + if (useOnyxAsDefault) { + chrome.tabs.create({}, () => { + window.close(); + }); + } else { + window.close(); + } + }, + ); + } + + // Load any existing values (in case user returns to this page) + function loadStoredValues() { + chrome.storage.local.get( + { + [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: "", + [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: true, + }, + (result) => { + if (result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]) { + domainInput.value = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; + } + useOnyxAsDefaultToggle.checked = + result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; + }, + ); + } + + if (themeToggle) { + themeToggle.addEventListener("click", toggleTheme); + } + + if (continueBtn) { + continueBtn.addEventListener("click", handleContinue); + } + + if (backBtn) { + backBtn.addEventListener("click", handleBack); + } + + if (finishBtn) { + finishBtn.addEventListener("click", handleFinish); + } + + // Allow Enter key to proceed + if (domainInput) { + domainInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + handleContinue(); + } + }); + } + + initTheme(); + loadStoredValues(); +}); diff --git a/extensions/chrome/src/styles/selection-icon.css b/extensions/chrome/src/styles/selection-icon.css new file mode 100644 index 0000000000..0cad29fad4 --- /dev/null +++ b/extensions/chrome/src/styles/selection-icon.css @@ -0,0 +1,42 @@ +#onyx-selection-icon { + position: fixed; + z-index: 2147483647; + width: 32px; + height: 32px; + border-radius: 50%; + background-color: #ffffff; + border: 1px solid #e0e0e0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: scale(0.8); + transition: + opacity 0.15s ease, + transform 0.15s ease, + box-shadow 0.15s ease; + pointer-events: none; +} + +#onyx-selection-icon.visible { + opacity: 1; + transform: scale(1); + pointer-events: auto; +} + +#onyx-selection-icon:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + transform: scale(1.1); +} + +#onyx-selection-icon:active { + transform: scale(0.95); +} + +#onyx-selection-icon img { + width: 20px; + height: 20px; + pointer-events: none; +} diff --git a/extensions/chrome/src/styles/shared.css b/extensions/chrome/src/styles/shared.css new file mode 100644 index 0000000000..ab1f5cb52b --- /dev/null +++ b/extensions/chrome/src/styles/shared.css @@ -0,0 +1,169 @@ +/* Import Hanken Grotesk font */ +@import url("https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@300;400;500;600;700&display=swap"); + +:root { + --primary-color: #4285f4; + --primary-hover-color: #3367d6; + --secondary-color: #f1f3f4; + --secondary-hover-color: #e8eaed; + --text-color: #333; + --text-light-color: #666; + --background-color: #f1f3f4; + --card-background-color: #fff; + --border-color: #ccc; + --font-family: Arial, sans-serif; + --font-hanken-grotesk: "Hanken Grotesk", sans-serif; +} + +body { + font-family: var(--font-hanken-grotesk); + margin: 0; + padding: 0; +} + +.container { + max-width: 500px; + width: 90%; + margin: 0 auto; +} + +.card { + background-color: var(--card-background-color); + padding: 25px; + border-radius: 10px; + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1); +} + +h1 { + color: var(--text-color); + font-size: 24px; + font-weight: 600; + margin-top: 0; + margin-bottom: 20px; +} + +.option-group { + margin-bottom: 20px; +} + +label { + display: block; + margin-bottom: 5px; + color: var(--text-light-color); + font-weight: 400; + font-size: 16px; +} + +input[type="text"] { + width: 100%; + padding: 8px; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 14px; + background-color: var(--card-background-color); + color: var(--text-color); +} + +.button { + width: 100%; + padding: 10px 20px; + border-radius: 5px; + border: none; + cursor: pointer; + font-size: 16px; + font-weight: 500; + transition: background-color 0.3s; +} + +.button.primary { + background-color: var(--primary-color); + color: #fff; +} + +.button.primary:hover { + background-color: var(--primary-hover-color); +} + +.button.secondary { + background-color: var(--secondary-color); + color: var(--text-color); +} + +.button.secondary:hover { + background-color: var(--secondary-hover-color); +} + +.status-container { + margin-top: 10px; + margin-bottom: 15px; +} + +.status-message { + margin: 0 0 10px 0; + color: var(--text-color); + font-weight: 500; + text-align: center; + font-size: 16px; + transition: opacity 0.5s ease-in-out; +} + +kbd { + background-color: var(--secondary-color); + border: 1px solid var(--border-color); + border-radius: 3px; + padding: 2px 5px; + font-family: monospace; + font-weight: 500; + color: var(--text-color); +} + +.toggle-label { + display: flex; + justify-content: space-between; + align-items: center; +} + +.toggle-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--secondary-color); + transition: 0.4s; + border-radius: 24px; +} + +.slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 2px; + bottom: 2px; + background-color: white; + transition: 0.4s; + border-radius: 50%; +} + +input:checked + .slider { + background-color: var(--primary-color); +} + +input:checked + .slider:before { + transform: translateX(26px); +} diff --git a/extensions/chrome/src/utils/constants.js b/extensions/chrome/src/utils/constants.js new file mode 100644 index 0000000000..b4a3c9035b --- /dev/null +++ b/extensions/chrome/src/utils/constants.js @@ -0,0 +1,43 @@ +export const THEMES = { + LIGHT: "light", + DARK: "dark", +}; + +export const DEFAULT_ONYX_DOMAIN = "http://localhost:3000"; + +export const SIDE_PANEL_PATH = "/chat/nrf/side-panel"; + +export const ACTIONS = { + GET_SELECTED_TEXT: "getSelectedText", + GET_CURRENT_ONYX_DOMAIN: "getCurrentOnyxDomain", + UPDATE_PAGE_URL: "updatePageUrl", + SEND_TO_ONYX: "sendToOnyx", + OPEN_SIDE_PANEL: "openSidePanel", + TOGGLE_NEW_TAB_OVERRIDE: "toggleNewTabOverride", + OPEN_SIDE_PANEL_WITH_INPUT: "openSidePanelWithInput", + OPEN_ONYX_WITH_INPUT: "openOnyxWithInput", + CLOSE_SIDE_PANEL: "closeSidePanel", +}; + +export const CHROME_SPECIFIC_STORAGE_KEYS = { + ONYX_DOMAIN: "onyxExtensionDomain", + USE_ONYX_AS_DEFAULT_NEW_TAB: "onyxExtensionDefaultNewTab", + THEME: "onyxExtensionTheme", + BACKGROUND_IMAGE: "onyxExtensionBackgroundImage", + DARK_BG_URL: "onyxExtensionDarkBgUrl", + LIGHT_BG_URL: "onyxExtensionLightBgUrl", + ONBOARDING_COMPLETE: "onyxExtensionOnboardingComplete", +}; + +export const CHROME_MESSAGE = { + PREFERENCES_UPDATED: "PREFERENCES_UPDATED", + ONYX_APP_LOADED: "ONYX_APP_LOADED", + SET_DEFAULT_NEW_TAB: "SET_DEFAULT_NEW_TAB", + LOAD_NEW_CHAT_PAGE: "LOAD_NEW_CHAT_PAGE", + LOAD_NEW_PAGE: "LOAD_NEW_PAGE", + AUTH_REQUIRED: "AUTH_REQUIRED", +}; + +export const WEB_MESSAGE = { + PAGE_CHANGE: "PAGE_CHANGE", +}; diff --git a/extensions/chrome/src/utils/content.js b/extensions/chrome/src/utils/content.js new file mode 100644 index 0000000000..97656cabbb --- /dev/null +++ b/extensions/chrome/src/utils/content.js @@ -0,0 +1,34 @@ +let sidePanel = null; + +function createSidePanel() { + sidePanel = document.createElement("div"); + sidePanel.id = "onyx-side-panel"; + sidePanel.style.cssText = ` + position: fixed; + top: 0; + right: -400px; + width: 400px; + height: 100%; + background-color: white; + box-shadow: -2px 0 5px rgba(0,0,0,0.2); + transition: right 0.3s ease-in-out; + z-index: 9999; + `; + + const iframe = document.createElement("iframe"); + iframe.style.cssText = ` + width: 100%; + height: 100%; + border: none; + `; + + chrome.runtime.sendMessage( + { action: ACTIONS.GET_CURRENT_ONYX_DOMAIN }, + function (response) { + iframe.src = response[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; + }, + ); + + sidePanel.appendChild(iframe); + document.body.appendChild(sidePanel); +} diff --git a/extensions/chrome/src/utils/error-modal.js b/extensions/chrome/src/utils/error-modal.js new file mode 100644 index 0000000000..49bb1b118e --- /dev/null +++ b/extensions/chrome/src/utils/error-modal.js @@ -0,0 +1,379 @@ +import { + CHROME_SPECIFIC_STORAGE_KEYS, + DEFAULT_ONYX_DOMAIN, + ACTIONS, +} from "./constants.js"; + +const errorModalHTML = ` +
+ + +
+`; + +const style = document.createElement("style"); +style.textContent = ` + :root { + --background-900: #0a0a0a; + --background-800: #1a1a1a; + --text-light-05: rgba(255, 255, 255, 0.95); + --text-light-03: rgba(255, 255, 255, 0.6); + --white-10: rgba(255, 255, 255, 0.1); + --white-15: rgba(255, 255, 255, 0.15); + --white-20: rgba(255, 255, 255, 0.2); + --white-30: rgba(255, 255, 255, 0.3); + } + + #error-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: none; + align-items: center; + justify-content: center; + z-index: 2000; + font-family: var(--font-hanken-grotesk), 'Hanken Grotesk', sans-serif; + } + + #error-modal .modal-backdrop { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(8px); + } + + #error-modal .modal-content { + position: relative; + background: linear-gradient(to bottom, rgba(10, 10, 10, 0.95), rgba(26, 26, 26, 0.95)); + backdrop-filter: blur(24px); + border-radius: 16px; + border: 1px solid var(--white-10); + max-width: 95%; + width: 500px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + overflow: hidden; + } + + #error-modal .modal-header { + padding: 24px; + border-bottom: 1px solid var(--white-10); + display: flex; + align-items: center; + gap: 12px; + } + + #error-modal .modal-icon { + width: 40px; + height: 40px; + border-radius: 12px; + background: rgba(255, 87, 87, 0.15); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + #error-modal .modal-icon svg { + width: 24px; + height: 24px; + stroke: #ff5757; + } + + #error-modal .modal-icon.auth-icon { + background: rgba(66, 133, 244, 0.15); + } + + #error-modal .modal-icon.auth-icon svg { + stroke: #4285f4; + } + + #error-modal h2 { + margin: 0; + color: var(--text-light-05); + font-size: 20px; + font-weight: 600; + } + + #error-modal .modal-body { + padding: 24px; + } + + #error-modal .modal-description { + color: var(--text-light-05); + margin: 0 0 20px 0; + font-size: 14px; + line-height: 1.6; + font-weight: 400; + } + + #error-modal .url-display { + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + padding: 12px; + border: 1px solid var(--white-10); + } + + #error-modal .url-label { + display: block; + font-size: 12px; + color: var(--text-light-03); + margin-bottom: 6px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + #error-modal .url-value { + display: block; + font-size: 13px; + color: var(--text-light-05); + word-break: break-all; + font-family: monospace; + line-height: 1.5; + } + + #error-modal .modal-footer { + padding: 0 24px 24px 24px; + } + + #error-modal .button-container { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 16px; + } + + #error-modal .button { + padding: 12px 20px; + border-radius: 8px; + border: none; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.2s; + font-family: var(--font-hanken-grotesk), 'Hanken Grotesk', sans-serif; + } + + #error-modal .button.primary { + background: rgba(255, 255, 255, 0.15); + color: var(--text-light-05); + border: 1px solid var(--white-10); + } + + #error-modal .button.primary:hover { + background: rgba(255, 255, 255, 0.2); + border-color: var(--white-20); + } + + #error-modal .button.secondary { + background: rgba(255, 255, 255, 0.05); + color: var(--text-light-05); + border: 1px solid var(--white-10); + } + + #error-modal .button.secondary:hover { + background: rgba(255, 255, 255, 0.1); + border-color: var(--white-15); + } + + #error-modal kbd { + background: rgba(255, 255, 255, 0.1); + border: 1px solid var(--white-10); + border-radius: 4px; + padding: 2px 6px; + font-family: monospace; + font-weight: 500; + color: var(--text-light-05); + font-size: 11px; + } + + @media (min-width: 768px) { + #error-modal .button-container { + flex-direction: row; + } + + #error-modal .button { + flex: 1; + } + } +`; + +const authModalHTML = ` +
+ + +
+`; + +let errorModal, attemptedUrlSpan, openOptionsButton, disableOverrideButton; + +let authModal, openAuthButton; + +export function initErrorModal() { + if (!document.getElementById("error-modal")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "../styles/shared.css"; + document.head.appendChild(link); + + document.body.insertAdjacentHTML("beforeend", errorModalHTML); + document.head.appendChild(style); + + errorModal = document.getElementById("error-modal"); + authModal = document.getElementById("error-modal"); + attemptedUrlSpan = document.getElementById("attempted-url"); + openOptionsButton = document.getElementById("open-options"); + disableOverrideButton = document.getElementById("disable-override"); + + openOptionsButton.addEventListener("click", (e) => { + e.preventDefault(); + chrome.runtime.openOptionsPage(); + }); + + disableOverrideButton.addEventListener("click", () => { + chrome.storage.local.set({ useOnyxAsDefaultNewTab: false }, () => { + chrome.tabs.update({ url: "chrome://new-tab-page" }); + }); + }); + } +} + +export function showErrorModal(url) { + if (!errorModal) { + initErrorModal(); + } + if (errorModal) { + errorModal.style.display = "flex"; + errorModal.style.zIndex = "9999"; + attemptedUrlSpan.textContent = url; + document.body.style.overflow = "hidden"; + } +} + +export function hideErrorModal() { + if (errorModal) { + errorModal.style.display = "none"; + document.body.style.overflow = "auto"; + } +} + +export function checkModalVisibility() { + return errorModal + ? window.getComputedStyle(errorModal).display !== "none" + : false; +} + +export function initAuthModal() { + if (!document.getElementById("error-modal")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "../styles/shared.css"; + document.head.appendChild(link); + + document.body.insertAdjacentHTML("beforeend", authModalHTML); + document.head.appendChild(style); + + authModal = document.getElementById("error-modal"); + openAuthButton = document.getElementById("open-auth"); + + openAuthButton.addEventListener("click", (e) => { + e.preventDefault(); + chrome.storage.local.get( + { [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN }, + (result) => { + const onyxDomain = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; + chrome.runtime.sendMessage( + { action: ACTIONS.CLOSE_SIDE_PANEL }, + () => { + if (chrome.runtime.lastError) { + console.error( + "Error closing side panel:", + chrome.runtime.lastError, + ); + } + chrome.tabs.create( + { + url: `${onyxDomain}/auth/login`, + active: true, + }, + (_) => { + if (chrome.runtime.lastError) { + console.error( + "Error opening auth tab:", + chrome.runtime.lastError, + ); + } + }, + ); + }, + ); + }, + ); + }); + } +} + +export function showAuthModal() { + if (!authModal) { + initAuthModal(); + } + if (authModal) { + authModal.style.display = "flex"; + authModal.style.zIndex = "9999"; + document.body.style.overflow = "hidden"; + } +} + +export function hideAuthModal() { + if (authModal) { + authModal.style.display = "none"; + document.body.style.overflow = "auto"; + } +} diff --git a/extensions/chrome/src/utils/selection-icon.js b/extensions/chrome/src/utils/selection-icon.js new file mode 100644 index 0000000000..e7a7baf8e6 --- /dev/null +++ b/extensions/chrome/src/utils/selection-icon.js @@ -0,0 +1,152 @@ +(function () { + const OPEN_SIDE_PANEL_WITH_INPUT = "openSidePanelWithInput"; + + let selectionIcon = null; + let currentSelectedText = ""; + + function createSelectionIcon() { + if (selectionIcon) return; + + selectionIcon = document.createElement("div"); + selectionIcon.id = "onyx-selection-icon"; + + const img = document.createElement("img"); + img.src = chrome.runtime.getURL("public/icon32.png"); + img.alt = "Search with Onyx"; + + selectionIcon.appendChild(img); + document.body.appendChild(selectionIcon); + + selectionIcon.addEventListener("mousedown", handleIconClick); + } + + function showIcon(text) { + if (!selectionIcon) { + createSelectionIcon(); + } + + currentSelectedText = text; + + const selection = window.getSelection(); + if (!selection.rangeCount) return; + + const range = selection.getRangeAt(0); + const rect = range.getBoundingClientRect(); + + const iconSize = 32; + const offset = 4; + + let posX = rect.right + offset; + let posY = rect.bottom + offset; + + if (posX + iconSize > window.innerWidth) { + posX = rect.left - iconSize - offset; + } + if (posY + iconSize > window.innerHeight) { + posY = rect.top - iconSize - offset; + } + + posX = Math.max( + offset, + Math.min(posX, window.innerWidth - iconSize - offset), + ); + posY = Math.max( + offset, + Math.min(posY, window.innerHeight - iconSize - offset), + ); + + selectionIcon.style.left = `${posX}px`; + selectionIcon.style.top = `${posY}px`; + selectionIcon.classList.add("visible"); + } + + function hideIcon() { + if (selectionIcon) { + selectionIcon.classList.remove("visible"); + } + currentSelectedText = ""; + } + + function handleIconClick(e) { + e.preventDefault(); + e.stopPropagation(); + + const textToSend = currentSelectedText; + + if (textToSend) { + chrome.runtime.sendMessage( + { + action: OPEN_SIDE_PANEL_WITH_INPUT, + selectedText: textToSend, + pageUrl: window.location.href, + }, + (response) => { + if (chrome.runtime.lastError) { + console.error( + "[Onyx] Error sending message:", + chrome.runtime.lastError.message, + ); + } else { + } + }, + ); + } + + hideIcon(); + } + + document.addEventListener("mouseup", (e) => { + if ( + e.target.id === "onyx-selection-icon" || + e.target.closest("#onyx-selection-icon") + ) { + return; + } + + setTimeout(() => { + const selection = window.getSelection(); + const selectedText = selection.toString().trim(); + + if (selectedText && selectedText.length > 0) { + showIcon(selectedText); + } else { + hideIcon(); + } + }, 10); + }); + + document.addEventListener("mousedown", (e) => { + if ( + e.target.id !== "onyx-selection-icon" && + !e.target.closest("#onyx-selection-icon") + ) { + const selection = window.getSelection(); + const selectedText = selection.toString().trim(); + if (!selectedText) { + hideIcon(); + } + } + }); + + document.addEventListener( + "scroll", + () => { + hideIcon(); + }, + true, + ); + + document.addEventListener("selectionchange", () => { + const selection = window.getSelection(); + const selectedText = selection.toString().trim(); + if (!selectedText) { + hideIcon(); + } + }); + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", createSelectionIcon); + } else { + createSelectionIcon(); + } +})(); diff --git a/extensions/chrome/src/utils/storage.js b/extensions/chrome/src/utils/storage.js new file mode 100644 index 0000000000..ff4b7faa38 --- /dev/null +++ b/extensions/chrome/src/utils/storage.js @@ -0,0 +1,24 @@ +import { + DEFAULT_ONYX_DOMAIN, + CHROME_SPECIFIC_STORAGE_KEYS, +} from "./constants.js"; + +export async function getOnyxDomain() { + const result = await chrome.storage.local.get({ + [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN, + }); + return result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; +} + +export function setOnyxDomain(domain, callback) { + chrome.storage.local.set( + { [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: domain }, + callback, + ); +} + +export function getOnyxDomainSync() { + return new Promise((resolve) => { + getOnyxDomain(resolve); + }); +}