diff --git a/options/options.css b/options/options.css new file mode 100644 index 0000000..6a81eab --- /dev/null +++ b/options/options.css @@ -0,0 +1,127 @@ +:root { + --fg: #15141a; + --fg-dim: #5b5b66; + --bg: transparent; + --border: #8f8f9d; + --field-bg: #ffffff; + --accent: #0060df; +} + +@media (prefers-color-scheme: dark) { + :root { + --fg: #fbfbfe; + --fg-dim: #b1b1bd; + --border: #8f8f9d; + --field-bg: #2b2a33; + --accent: #0df; + } +} + +body { + margin: 0; + padding: 0.5rem 0; + color: var(--fg); + background: var(--bg); + font: message-box; + font-size: 1rem; + line-height: 1.4; + max-width: 42rem; +} + +.field { + margin-bottom: 1.15rem; +} + +label { + display: block; + font-weight: 600; + margin-bottom: 0.3rem; +} + +.checkbox label { + font-weight: 400; + display: flex; + align-items: baseline; + gap: 0.45rem; +} + +input[type="text"], +select { + width: 100%; + max-width: 100%; + box-sizing: border-box; + padding: 0.4rem 0.5rem; + color: var(--fg); + background: var(--field-bg); + border: 1px solid var(--border); + border-radius: 4px; + font: inherit; +} + +input[type="text"]:focus-visible, +select:focus-visible, +button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 1px; +} + +.hint { + margin: 0.3rem 0 0; + color: var(--fg-dim); + font-size: 0.85rem; +} + +.tag { + font-weight: 400; + font-size: 0.75rem; + color: var(--fg-dim); + border: 1px solid var(--border); + border-radius: 3px; + padding: 0 0.3rem; + vertical-align: 1px; +} + +.actions { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; + margin-top: 1.5rem; +} + +button { + padding: 0.35rem 0.8rem; + color: var(--fg); + background: var(--field-bg); + border: 1px solid var(--border); + border-radius: 4px; + font: inherit; + cursor: pointer; +} + +#status { + color: var(--fg-dim); + font-size: 0.85rem; +} + +.note { + margin-top: 1.75rem; + padding-top: 1rem; + border-top: 1px solid var(--border); + color: var(--fg-dim); + font-size: 0.85rem; +} + +code, +kbd { + font-family: monospace; + font-size: 0.9em; +} + +/* The inline about:addons surface can be very narrow. */ +@media (max-width: 26rem) { + .actions { + align-items: flex-start; + flex-direction: column; + } +} diff --git a/options/options.html b/options/options.html new file mode 100644 index 0000000..cebdd4a --- /dev/null +++ b/options/options.html @@ -0,0 +1,60 @@ + + + +
+ Selecting text and choosing Ask ChatGPT — or pressing + Alt+Shift+G — opens ChatGPT with your text + filled in. ChatGPT no longer submits prompts that arrive from a link, so press + Enter there to send. Change the shortcut under + Manage Extension Shortcuts in Add-ons. +
+ + diff --git a/options/options.js b/options/options.js new file mode 100644 index 0000000..3afa0a7 --- /dev/null +++ b/options/options.js @@ -0,0 +1,83 @@ +import { DEFAULT_SETTINGS } from "../lib/defaults.js"; +import { load, save } from "../lib/settings.js"; + +const SAVE_DEBOUNCE_MS = 400; +const STATUS_CLEAR_MS = 1500; + +const field = (id) => document.getElementById(id); + +let saveTimer = null; +let statusTimer = null; +// Suppresses the change/input handlers while we are programmatically writing +// values into the form, so a remote sync cannot trigger a save loop. +let applying = false; + +function render(settings) { + applying = true; + field("model").value = settings.model; + field("temporaryChat").checked = settings.temporaryChat; + field("webSearch").checked = settings.webSearch; + field("openIn").value = settings.openIn; + field("promptTemplate").value = settings.promptTemplate; + applying = false; +} + +function collect() { + return { + model: field("model").value, + temporaryChat: field("temporaryChat").checked, + webSearch: field("webSearch").checked, + openIn: field("openIn").value, + promptTemplate: field("promptTemplate").value, + }; +} + +function showStatus(message) { + field("status").textContent = message; + clearTimeout(statusTimer); + statusTimer = setTimeout(() => { + field("status").textContent = ""; + }, STATUS_CLEAR_MS); +} + +async function persist(settings) { + try { + await save(settings); + showStatus("Saved"); + } catch { + // storage.sync sanitises its errors, so there is nothing specific to report. + showStatus("Could not save settings"); + } +} + +// Debounced so typing in a text field cannot approach any write-rate quota. +function scheduleSave() { + if (applying) { + return; + } + clearTimeout(saveTimer); + saveTimer = setTimeout(() => persist(collect()), SAVE_DEBOUNCE_MS); +} + +// Module scripts are deferred, so the DOM is already parsed here. +render(await load()); + +for (const id of Object.keys(DEFAULT_SETTINGS)) { + field(id).addEventListener("input", scheduleSave); + field(id).addEventListener("change", scheduleSave); +} + +field("restore").addEventListener("click", () => { + clearTimeout(saveTimer); + render(DEFAULT_SETTINGS); + persist({ ...DEFAULT_SETTINGS }); +}); + +// A remote sync landing while the panel is open must update the controls rather +// than being clobbered by stale field values on the next edit. +browser.storage.onChanged.addListener(async (changes, area) => { + if (area !== "sync") { + return; + } + render(await load()); +});