mirror of
https://git.zavage.net/Zavage-Software/search-with-chatgpt-powered-by-openai-extension.git
synced 2026-08-10 13:40:29 -06:00
feat: add options panel embedded in about:addons
This commit is contained in:
parent
6bedd700e8
commit
b42b11755c
127
options/options.css
Normal file
127
options/options.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
60
options/options.html
Normal file
60
options/options.html
Normal file
@ -0,0 +1,60 @@
|
||||
<!-- Rendered inline inside about:addons. Keep it narrow-friendly and do not
|
||||
rely on window sizing; a pref can degrade this to a standalone tab. -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Search with ChatGPT — Options</title>
|
||||
<link rel="stylesheet" href="options.css" />
|
||||
|
||||
<form id="settings" autocomplete="off">
|
||||
<div class="field">
|
||||
<label for="model">Model <span class="tag">experimental</span></label>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="field checkbox">
|
||||
<label><input type="checkbox" id="temporaryChat" /> Request a temporary chat</label>
|
||||
<p class="hint">
|
||||
ChatGPT may not honour this. Check ChatGPT's own indicator before sending
|
||||
anything sensitive.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field checkbox">
|
||||
<label><input type="checkbox" id="webSearch" /> Ask ChatGPT to use web search (hint only)</label>
|
||||
<p class="hint">A hint to ChatGPT's interface, not a guarantee.</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="openIn">Open in</label>
|
||||
<select id="openIn">
|
||||
<option value="new-tab">A new tab</option>
|
||||
<option value="background-tab">A new background tab</option>
|
||||
<option value="new-window">A new window</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" id="restore">Restore defaults</button>
|
||||
<span id="status" role="status" aria-live="polite"></span>
|
||||
</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>
|
||||
|
||||
<script type="module" src="options.js"></script>
|
||||
83
options/options.js
Normal file
83
options/options.js
Normal file
@ -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());
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user