feat: add advanced context actions and optional translation
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
body { margin: 0; padding: 1.25rem; box-sizing: border-box; }
|
||||
h1 { font-size: 1.25rem; margin-top: 0; }
|
||||
[hidden] { display: none !important; }
|
||||
button:disabled { opacity: 0.6; }
|
||||
#message { overflow-wrap: anywhere; }
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Ask ChatGPT</title>
|
||||
<link rel="stylesheet" href="../options/options.css">
|
||||
<link rel="stylesheet" href="action.css">
|
||||
<h1 id="title">Ask ChatGPT</h1>
|
||||
<p id="description">Preparing your action…</p>
|
||||
<form id="action-form">
|
||||
<div id="language-field" class="field" hidden>
|
||||
<label for="language">Translate into</label>
|
||||
<input id="language" type="text" autocomplete="off" placeholder="For example, Spanish" aria-describedby="message">
|
||||
</div>
|
||||
<p id="message" role="status" aria-live="polite"></p>
|
||||
<div class="actions">
|
||||
<button id="continue" type="submit" hidden>Continue</button>
|
||||
<button id="cancel" type="button">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
<script type="module" src="action.js"></script>
|
||||
</html>
|
||||
@@ -0,0 +1,51 @@
|
||||
import { createDialogController } from './controller.js';
|
||||
import { openChat } from '../background/open-chat.js';
|
||||
|
||||
const field = id => document.getElementById(id);
|
||||
let controller;
|
||||
let initialized = false;
|
||||
let previousKind;
|
||||
const close = () => window.close();
|
||||
function render(state) {
|
||||
const language = state.kind === 'language';
|
||||
field('title').textContent = language ? 'Translate selected text' : 'Ask ChatGPT';
|
||||
field('description').textContent = language ? 'Which language would you like to translate into?'
|
||||
: state.kind === 'shortened' ? 'The selection is too long to send in full. Continue with a shortened selection, or cancel and select less text.'
|
||||
: state.kind === 'error' ? state.message : 'Ready to open ChatGPT with your prompt.';
|
||||
field('language-field').hidden = !language;
|
||||
field('language').disabled = state.busy;
|
||||
field('continue').hidden = state.kind === 'error';
|
||||
field('continue').disabled = state.busy;
|
||||
field('continue').textContent = state.kind === 'shortened' ? 'Continue with shortened text' : 'Continue';
|
||||
field('cancel').disabled = state.busy;
|
||||
field('message').textContent = state.kind === 'error' ? '' : state.message ?? '';
|
||||
if (previousKind !== state.kind) {
|
||||
(language ? field('language') : state.kind === 'error' ? field('cancel') : field('continue')).focus();
|
||||
previousKind = state.kind;
|
||||
}
|
||||
}
|
||||
function failed() {
|
||||
if (initialized) return;
|
||||
render({ kind: 'error', message: 'Could not prepare this action. Close this window and try the menu action again.' });
|
||||
}
|
||||
const id = new URL(location.href).searchParams.get('request');
|
||||
const timer = setTimeout(failed, 15000);
|
||||
const port = browser.runtime.connect({ name: 'action-dialog:' + id });
|
||||
port.onMessage.addListener(message => {
|
||||
if (initialized || message?.type !== 'initialize' || !message.payload?.request) return;
|
||||
initialized = true;
|
||||
clearTimeout(timer);
|
||||
controller = createDialogController({ payload: message.payload, render, close,
|
||||
open: (url, mode) => openChat(browser, url, mode) });
|
||||
port.postMessage({ type: 'received' });
|
||||
});
|
||||
port.onDisconnect.addListener(() => { clearTimeout(timer); failed(); });
|
||||
field('action-form').addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
void controller?.submit(field('language').value);
|
||||
});
|
||||
function cancel() { if (controller) controller.cancel(); else close(); }
|
||||
field('cancel').addEventListener('click', cancel);
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape') { event.preventDefault(); cancel(); }
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { normalizeLanguage } from '../lib/settings.js';
|
||||
import { prepareAction } from '../lib/prepare-action.js';
|
||||
|
||||
export function createDialogController({ payload, open, render, close }) {
|
||||
let request = structuredClone(payload.request);
|
||||
let state = { ...payload.preparation, busy: false };
|
||||
let cancelled = false;
|
||||
const show = () => render({ ...state });
|
||||
show();
|
||||
return {
|
||||
cancel() {
|
||||
if (state.busy || cancelled) return;
|
||||
cancelled = true;
|
||||
request = null;
|
||||
close();
|
||||
},
|
||||
async submit(language) {
|
||||
if (cancelled || state.busy || state.kind === 'error') return;
|
||||
if (state.kind === 'language') {
|
||||
const normalized = normalizeLanguage(language);
|
||||
if (!normalized) {
|
||||
state.message = 'Enter a language using up to 100 characters.';
|
||||
show(); return;
|
||||
}
|
||||
request.language = normalized;
|
||||
state = { ...prepareAction(request), busy: false };
|
||||
show();
|
||||
if (state.kind !== 'ready') return;
|
||||
}
|
||||
if (!['ready', 'shortened'].includes(state.kind)) return;
|
||||
state.busy = true;
|
||||
state.message = '';
|
||||
show();
|
||||
try {
|
||||
await open(state.url, request.settings.openIn);
|
||||
cancelled = true;
|
||||
request = null;
|
||||
close();
|
||||
} catch {
|
||||
state.busy = false;
|
||||
state.message = 'Could not open ChatGPT. Please try again or cancel.';
|
||||
show();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user