feat: add advanced context actions and optional translation
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { normalizeLanguage } from '../lib/settings.js';
|
||||
|
||||
export const MENU_SETTING_KEYS = ['contextMenuMode', 'showTranslate', 'translationMode', 'translationLanguage'];
|
||||
export function initializeMenuSettings({ document, onChange }) {
|
||||
const field = id => document.getElementById(id);
|
||||
function update() {
|
||||
const advanced = field('contextMenuMode').value === 'advanced';
|
||||
const translation = advanced && field('showTranslate').checked;
|
||||
const preferred = translation && field('translationMode').value === 'preferred';
|
||||
field('advanced-settings').hidden = !advanced;
|
||||
field('translation-settings').hidden = !translation;
|
||||
field('preferred-language-field').hidden = !preferred;
|
||||
const valid = Boolean(normalizeLanguage(field('translationLanguage').value));
|
||||
field('language-validation').textContent = preferred && !valid
|
||||
? 'Enter a language using up to 100 characters. Until then, Translate will ask each time.' : '';
|
||||
field('translationLanguage').setAttribute('aria-invalid', String(preferred && !valid));
|
||||
}
|
||||
for (const id of MENU_SETTING_KEYS) {
|
||||
const changed = () => { update(); onChange(); };
|
||||
field(id).addEventListener('input', changed);
|
||||
field(id).addEventListener('change', changed);
|
||||
}
|
||||
return {
|
||||
render(settings) {
|
||||
for (const id of MENU_SETTING_KEYS) {
|
||||
const element = field(id);
|
||||
if (element === document.activeElement) continue;
|
||||
if (id === 'showTranslate') element.checked = settings[id];
|
||||
else element.value = settings[id];
|
||||
}
|
||||
update();
|
||||
},
|
||||
collect() {
|
||||
return {
|
||||
contextMenuMode: field('contextMenuMode').value,
|
||||
showTranslate: field('showTranslate').checked,
|
||||
translationMode: field('translationMode').value,
|
||||
translationLanguage: normalizeLanguage(field('translationLanguage').value),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
+56
-3
@@ -1,7 +1,7 @@
|
||||
:root {
|
||||
--fg: #15141a;
|
||||
--fg-dim: #5b5b66;
|
||||
--bg: transparent;
|
||||
--fg-dim: #38383d;
|
||||
--bg: #ffffff;
|
||||
--border: #8f8f9d;
|
||||
--field-bg: #ffffff;
|
||||
--accent: #0060df;
|
||||
@@ -10,7 +10,8 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--fg: #fbfbfe;
|
||||
--fg-dim: #b1b1bd;
|
||||
--fg-dim: #e0e0e6;
|
||||
--bg: #23222b;
|
||||
--border: #8f8f9d;
|
||||
--field-bg: #2b2a33;
|
||||
--accent: #0df;
|
||||
@@ -28,6 +29,10 @@ body {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 1.15rem;
|
||||
}
|
||||
@@ -118,6 +123,47 @@ kbd {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.note h2 {
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--fg);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.note p {
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.note code {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.project-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 0.65rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.project-link {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.link-separator {
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* The inline about:addons surface can be very narrow. */
|
||||
@media (max-width: 26rem) {
|
||||
.actions {
|
||||
@@ -125,3 +171,10 @@ kbd {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
[hidden] { display: none !important; }
|
||||
.dependent-settings {
|
||||
border-inline-start: 2px solid var(--border);
|
||||
padding-inline-start: 1rem;
|
||||
margin-bottom: 1.15rem;
|
||||
}
|
||||
|
||||
+77
-16
@@ -6,16 +6,44 @@
|
||||
<title>Search with ChatGPT — Options</title>
|
||||
<link rel="stylesheet" href="options.css" />
|
||||
|
||||
<p class="intro">
|
||||
This extension adds <a href="https://chatgpt.com/" target="_blank" rel="noopener noreferrer">ChatGPT</a> as a Firefox search engine. Type
|
||||
<code>gpt your question</code> in the address bar, select text on a webpage and
|
||||
choose <strong>Ask ChatGPT</strong> from the right-click menu.<span id="shortcut-intro"></span> If the prompt isn’t sent automatically, press Enter in
|
||||
ChatGPT.
|
||||
</p>
|
||||
|
||||
<form id="settings" autocomplete="off">
|
||||
<div class="field">
|
||||
<label for="model">Model <span class="tag">experimental</span></label>
|
||||
<!-- maxlength is set from MAX_MODEL_CHARS in options.js, so the input and
|
||||
mergeSettings() can never disagree about the bound. -->
|
||||
<input type="text" id="model" placeholder="auto" spellcheck="false" />
|
||||
<p class="hint">
|
||||
ChatGPT may ignore this and use your account default. Leave blank to
|
||||
always use your account default.
|
||||
</p>
|
||||
<label for="contextMenuMode">Context menu</label>
|
||||
<select id="contextMenuMode">
|
||||
<option value="off">Off</option>
|
||||
<option value="simple">Simple</option>
|
||||
<option value="advanced">Advanced</option>
|
||||
</select>
|
||||
<p class="hint">Simple asks about selected text. Advanced adds Explain, Summarize, Debug code, and link/page actions.</p>
|
||||
<p class="hint" id="shortcut-hint">The keyboard shortcut always runs your default action.</p>
|
||||
</div>
|
||||
|
||||
<div id="advanced-settings" class="dependent-settings" hidden>
|
||||
<div class="field checkbox">
|
||||
<label><input type="checkbox" id="showTranslate"> Show Translate in advanced menu</label>
|
||||
</div>
|
||||
<div id="translation-settings" hidden>
|
||||
<div class="field">
|
||||
<label for="translationMode">Translate into</label>
|
||||
<select id="translationMode">
|
||||
<option value="ask">Ask each time</option>
|
||||
<option value="preferred">Preferred language</option>
|
||||
</select>
|
||||
<p class="hint">Ask each time opens a language dialog. A preferred language opens ChatGPT directly.</p>
|
||||
</div>
|
||||
<div class="field" id="preferred-language-field" hidden>
|
||||
<label for="translationLanguage">Preferred language</label>
|
||||
<input type="text" id="translationLanguage" placeholder="For example, Spanish" aria-describedby="language-validation">
|
||||
<p class="hint" id="language-validation" aria-live="polite"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field checkbox">
|
||||
@@ -43,7 +71,7 @@
|
||||
<div class="field">
|
||||
<label for="promptTemplate">Prompt template</label>
|
||||
<input type="text" id="promptTemplate" spellcheck="false" />
|
||||
<p class="hint"><code>{query}</code> is replaced with your selected text.</p>
|
||||
<p class="hint"><code>{query}</code> is replaced with your selected text. Used by Ask normally, Simple mode, and the keyboard shortcut. Other advanced actions use their own prompts.</p>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
@@ -52,12 +80,45 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="note">
|
||||
Selecting text and choosing <strong>Ask ChatGPT</strong> — or pressing
|
||||
<kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd> — 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
|
||||
<em>Manage Extension Shortcuts</em> in Add-ons.
|
||||
</p>
|
||||
<section class="note" aria-labelledby="search-settings-title">
|
||||
<h2 id="search-settings-title">Search keyword and default engine</h2>
|
||||
<p>
|
||||
Open Firefox’s <strong>Settings → Search</strong>, or paste
|
||||
<code>about:preferences#search</code> into the address bar.
|
||||
Under <strong>Search Shortcuts</strong>, edit ChatGPT’s keyword to change
|
||||
<code>gpt</code>. Under <strong>Default Search Engine</strong>, choose
|
||||
ChatGPT to use it for address-bar searches without a prefix. Choose your
|
||||
previous engine there to switch back.
|
||||
<a href="https://support.mozilla.org/en-US/kb/change-your-default-search-settings-firefox" target="_blank" rel="noopener noreferrer">Firefox search settings guide</a>.
|
||||
</p>
|
||||
<p>
|
||||
The settings above apply to the right-click menu and keyboard shortcut;
|
||||
address-bar searches use Firefox’s search engine settings.
|
||||
Restore defaults only resets extension settings, including the prompt
|
||||
template; it does not change Firefox’s search engine or keyboard shortcut.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="note" aria-labelledby="shortcut-title">
|
||||
<h2 id="shortcut-title">Keyboard shortcut</h2>
|
||||
<p id="shortcut-current" role="status" aria-live="polite">Checking keyboard shortcut…</p>
|
||||
<button type="button" id="change-shortcut" hidden>Change keyboard shortcut</button>
|
||||
<p class="hint">
|
||||
You can also open Add-ons, then the gear menu →
|
||||
<em>Manage Extension Shortcuts</em> to change or remove the shortcut.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<nav class="project-links" aria-label="Project links">
|
||||
<a href="https://zavage-software.com/portfolio/search-with-ChatGPT" target="_blank" rel="noopener noreferrer">Project website</a>
|
||||
<span class="project-link">
|
||||
<span class="link-separator" aria-hidden="true">|</span>
|
||||
<a href="https://git-repos.zavage.net/zavage-software/search-with-chatgpt-powered-by-openai-extension" target="_blank" rel="noopener noreferrer">Source code</a>
|
||||
</span>
|
||||
<span class="project-link">
|
||||
<span class="link-separator" aria-hidden="true">|</span>
|
||||
<a href="https://zavage-software.com/contact-us" target="_blank" rel="noopener noreferrer">Feedback or report a bug</a>
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
<script type="module" src="options.js"></script>
|
||||
|
||||
+10
-7
@@ -1,5 +1,7 @@
|
||||
import { DEFAULT_SETTINGS, MAX_MODEL_CHARS } from "../lib/defaults.js";
|
||||
import { DEFAULT_SETTINGS } from "../lib/defaults.js";
|
||||
import { load, save } from "../lib/settings.js";
|
||||
import { initializeMenuSettings, MENU_SETTING_KEYS } from "./menu-settings.js";
|
||||
import { initializeShortcut } from "./shortcut.js";
|
||||
|
||||
const SAVE_DEBOUNCE_MS = 400;
|
||||
const STATUS_CLEAR_MS = 1500;
|
||||
@@ -9,8 +11,12 @@ const field = (id) => document.getElementById(id);
|
||||
let saveTimer = null;
|
||||
let statusTimer = null;
|
||||
|
||||
const menuSettings = initializeMenuSettings({ document, onChange: scheduleSave });
|
||||
|
||||
function render(settings) {
|
||||
menuSettings.render(settings);
|
||||
for (const [id, value] of Object.entries(settings)) {
|
||||
if (MENU_SETTING_KEYS.includes(id)) continue;
|
||||
const element = field(id);
|
||||
// Never overwrite the control the user is actively editing — this page's
|
||||
// own saves echo back through storage.onChanged (see below).
|
||||
@@ -27,7 +33,7 @@ function render(settings) {
|
||||
|
||||
function collect() {
|
||||
return {
|
||||
model: field("model").value,
|
||||
...menuSettings.collect(),
|
||||
temporaryChat: field("temporaryChat").checked,
|
||||
webSearch: field("webSearch").checked,
|
||||
openIn: field("openIn").value,
|
||||
@@ -59,15 +65,12 @@ function scheduleSave() {
|
||||
saveTimer = setTimeout(() => persist(collect()), SAVE_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// Derived rather than hard-coded in the markup: if the two ever disagreed, the
|
||||
// input would accept a model that mergeSettings() then silently discards,
|
||||
// blanking the setting with no explanation.
|
||||
field("model").maxLength = MAX_MODEL_CHARS;
|
||||
|
||||
// Module scripts are deferred, so the DOM is already parsed here.
|
||||
void initializeShortcut({ commands: browser.commands, document, window });
|
||||
render(await load());
|
||||
|
||||
for (const id of Object.keys(DEFAULT_SETTINGS)) {
|
||||
if (MENU_SETTING_KEYS.includes(id)) continue;
|
||||
field(id).addEventListener("input", scheduleSave);
|
||||
field(id).addEventListener("change", scheduleSave);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Firefox owns shortcut preferences; never copy them into extension storage.
|
||||
export async function initializeShortcut({ commands, document, window }) {
|
||||
const field = (id) => document.getElementById(id);
|
||||
let refreshId = 0;
|
||||
|
||||
function render(shortcut) {
|
||||
field("shortcut-intro").textContent = shortcut
|
||||
? ` Or press ${shortcut} to use your selection. With nothing selected, the shortcut opens a blank chat.`
|
||||
: "";
|
||||
field("shortcut-hint").textContent = shortcut
|
||||
? `${shortcut} runs your default action even when the context menu is off.`
|
||||
: "The keyboard shortcut runs your default action independently of the context menu.";
|
||||
field("shortcut-current").textContent = shortcut === null
|
||||
? "Could not read the keyboard shortcut. Check Manage Extension Shortcuts in Add-ons."
|
||||
: shortcut ? `Current shortcut: ${shortcut}` : "No keyboard shortcut assigned.";
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const requestId = ++refreshId;
|
||||
try {
|
||||
const all = await commands.getAll();
|
||||
const command = all.find(({ name }) => name === "ask-chatgpt");
|
||||
if (requestId === refreshId) render(command?.shortcut ?? "");
|
||||
} catch {
|
||||
if (requestId === refreshId) render(null);
|
||||
}
|
||||
}
|
||||
|
||||
const button = field("change-shortcut");
|
||||
button.hidden = typeof commands?.openShortcutSettings !== "function";
|
||||
button.addEventListener("click", async () => {
|
||||
try {
|
||||
await commands.openShortcutSettings();
|
||||
} catch {
|
||||
field("shortcut-current").textContent =
|
||||
"Open Add-ons, then the gear menu → Manage Extension Shortcuts to change your shortcut.";
|
||||
}
|
||||
});
|
||||
|
||||
commands?.onChanged?.addListener(({ name }) => {
|
||||
if (name === "ask-chatgpt") void refresh();
|
||||
});
|
||||
window.addEventListener("focus", refresh);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.visibilityState === "visible") void refresh();
|
||||
});
|
||||
await refresh();
|
||||
}
|
||||
Reference in New Issue
Block a user