diff options
| author | Bobby <[email protected]> | 2025-09-24 16:14:00 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2025-09-24 16:14:00 +0530 |
| commit | 8f17548fc0e1b41d725f2144e209ac8655d2afeb (patch) | |
| tree | 8e9d1bc4fb45c47719f320b94f30f4b4222bb513 | |
| parent | fbfb98c05d201c414fcb58a864a4aba1aa8e28f4 (diff) | |
| download | thunderbird-ai-compose-8f17548fc0e1b41d725f2144e209ac8655d2afeb.tar.xz thunderbird-ai-compose-8f17548fc0e1b41d725f2144e209ac8655d2afeb.zip | |
improved styles. added icons to manifest. generate logic in place
| -rw-r--r-- | src/manifest.json | 12 | ||||
| -rw-r--r-- | src/options/options.html | 58 | ||||
| -rw-r--r-- | src/popup/popup.html | 92 | ||||
| -rw-r--r-- | src/popup/popup.ts | 96 |
4 files changed, 209 insertions, 49 deletions
diff --git a/src/manifest.json b/src/manifest.json index fb7c621..b0b4635 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -15,9 +15,19 @@ "background.js" ] }, + "icons": { + "16": "icons/16x16.png", + "32": "icons/32x32.png", + "48": "icons/48x48.png", + "128": "icons/128x128.png" + }, "compose_action": { "default_title": "Write with AI", - "default_popup": "popup/popup.html" + "default_popup": "popup/popup.html", + "default_icon": { + "16": "icons/16x16.png", + "32": "icons/32x32.png" + } }, "options_ui": { "page": "options/options.html", diff --git a/src/options/options.html b/src/options/options.html index bf70f4c..d317f1e 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -5,27 +5,71 @@ <title>AI Compose Settings</title> <style> body { - font-family: sans-serif; - padding: 10px; + font-family: system-ui, sans-serif; + margin: 0; + padding: 16px; + width: 360px; + background: var(--bg); + color: var(--fg); + } + h2 { + margin-top: 0; + font-size: 1.1rem; } label { display: block; - margin: 10px 0 4px; + margin: 12px 0 6px; + font-size: 0.9rem; } input { width: 100%; - padding: 6px; + padding: 8px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--input-bg); + color: var(--fg); } button { + margin-top: 14px; + padding: 8px 16px; + border: none; + border-radius: 6px; + background: var(--accent); + color: white; + font-weight: 500; + cursor: pointer; + } + p#msg { margin-top: 10px; - padding: 6px 12px; + font-size: 0.85rem; + color: var(--accent); + } + @media (prefers-color-scheme: light) { + :root { + --bg: #ffffff; + --fg: #111111; + --border: #cccccc; + --input-bg: #f9f9f9; + --accent: #3b82f6; + } + } + @media (prefers-color-scheme: dark) { + :root { + --bg: #1e1e1e; + --fg: #f5f5f5; + --border: #444444; + --input-bg: #2a2a2a; + --accent: #60a5fa; + } } </style> </head> <body> <h2>Settings</h2> - <label>Proxy Endpoint <input id="endpoint" type="url" /></label> - <label>Auth Token <input id="token" type="text" /></label> + <label for="endpoint">Proxy Endpoint</label> + <input id="endpoint" type="url" /> + <label for="token">Auth Token</label> + <input id="token" type="text" /> <button id="save">Save</button> <p id="msg"></p> diff --git a/src/popup/popup.html b/src/popup/popup.html index 808bc9e..c8838ba 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -2,34 +2,92 @@ <html> <head> <meta charset="utf-8" /> - <title>AI Prompt</title> + <title>AI Compose</title> <style> body { - font-family: sans-serif; - padding: 10px; + font-family: system-ui, sans-serif; + margin: 0; + padding: 16px; + width: 360px; + background: var(--bg); + color: var(--fg); + } + h2 { + margin-top: 0; + font-size: 1.1rem; } textarea { - width: 100%; - height: 120px; + width: calc(100% - 16px); + min-height: 120px; + resize: vertical; + padding: 8px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--input-bg); + color: var(--fg); + font-size: 0.9rem; + line-height: 1.4; + } + .actions { + margin-top: 14px; + display: flex; + align-items: center; + gap: 8px; } button { - margin-top: 8px; - padding: 6px 12px; + padding: 8px 16px; + border: none; + border-radius: 6px; + background: var(--accent); + color: white; + font-weight: 500; + cursor: pointer; + } + button:disabled { + opacity: 0.6; + cursor: not-allowed; + } + #spinner { + display: none; + width: 18px; + height: 18px; + border: 2px solid rgba(0, 0, 0, 0.2); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.6s linear infinite; + } + @keyframes spin { + to { + transform: rotate(360deg); + } + } + @media (prefers-color-scheme: light) { + :root { + --bg: #ffffff; + --fg: #111111; + --border: #cccccc; + --input-bg: #f9f9f9; + --accent: #3b82f6; + } } - pre { - background: #f5f5f5; - padding: 6px; - max-height: 200px; - overflow: auto; + @media (prefers-color-scheme: dark) { + :root { + --bg: #1e1e1e; + --fg: #f5f5f5; + --border: #444444; + --input-bg: #2a2a2a; + --accent: #60a5fa; + } } </style> </head> <body> - <h3>Enter Prompt</h3> - <textarea id="prompt"></textarea> - <br /> - <button id="send">Show Compose Context</button> - <pre id="output">No data yet</pre> + <h2>Compose with AI</h2> + <textarea id="prompt" placeholder="Write your instructions here..."></textarea> + <div class="actions"> + <button id="send" disabled>Insert</button> + <div id="spinner"></div> + </div> <script src="../browser-polyfill.js"></script> <script src="popup.js"></script> diff --git a/src/popup/popup.ts b/src/popup/popup.ts index dc28bdc..e3d4e29 100644 --- a/src/popup/popup.ts +++ b/src/popup/popup.ts @@ -5,9 +5,22 @@ interface Payload { context: ComposeContext; } -const promptEl: HTMLTextAreaElement = document.getElementById("prompt") as HTMLTextAreaElement; -const sendBtn: HTMLButtonElement = document.getElementById("send") as HTMLButtonElement; -const outputEl: HTMLElement = document.getElementById("output") as HTMLElement; +interface Settings { + proxyEndpoint?: string; + proxyToken?: string; +} + +function getElements(): { + promptEl: HTMLTextAreaElement; + sendBtn: HTMLButtonElement; + spinner: HTMLElement; +} { + return { + promptEl: document.getElementById("prompt") as HTMLTextAreaElement, + sendBtn: document.getElementById("send") as HTMLButtonElement, + spinner: document.getElementById("spinner") as HTMLElement, + }; +} async function getActiveComposeTabId(): Promise<number | undefined> { const tabs: browser.tabs.Tab[] = (await browser.tabs.query({ @@ -22,26 +35,17 @@ function insertAtReply(body: string, aiText: string, isHtml: boolean): string { const parser: DOMParser = new DOMParser(); const doc: Document = parser.parseFromString(body, "text/html"); const paragraphs: NodeListOf<HTMLParagraphElement> = doc.querySelectorAll("body > p"); - - let target: HTMLParagraphElement | null = null; for (const p of Array.from(paragraphs)) { if (p.textContent && p.textContent.trim().length > 0) { - target = p; - break; + p.insertAdjacentHTML("afterend", aiText); + return doc.body.innerHTML; } } - - if (target) { - target.insertAdjacentHTML("afterend", aiText); - return doc.body.innerHTML; - } - const citeDiv: HTMLElement | null = doc.querySelector("div.moz-cite-prefix"); if (citeDiv) { citeDiv.insertAdjacentHTML("beforebegin", aiText); return doc.body.innerHTML; } - doc.body.insertAdjacentHTML("afterbegin", aiText); return doc.body.innerHTML; } else { @@ -54,6 +58,11 @@ function insertAtReply(body: string, aiText: string, isHtml: boolean): string { } async function handleInsert(): Promise<void> { + const { promptEl, sendBtn, spinner } = getElements(); + spinner.style.display = "block"; + sendBtn.disabled = true; + promptEl.disabled = true; + const ctx: ComposeContext = (await browser.runtime.sendMessage({ type: "getComposeContext", })) as ComposeContext; @@ -63,32 +72,71 @@ async function handleInsert(): Promise<void> { context: ctx, }; + const settings: Settings = await browser.storage.local.get(["proxyEndpoint", "proxyToken"]); + if (!settings.proxyEndpoint) { + alert("Proxy endpoint is not configured in settings."); + spinner.style.display = "none"; + sendBtn.disabled = promptEl.value.trim().length === 0; + promptEl.disabled = false; + return; + } + const tabId: number | undefined = await getActiveComposeTabId(); if (tabId === undefined) { - outputEl.textContent = "No active compose tab."; + alert("No active compose tab."); + spinner.style.display = "none"; + sendBtn.disabled = promptEl.value.trim().length === 0; + promptEl.disabled = false; + return; + } + + let aiResult: string; + try { + const res: Response = await fetch(settings.proxyEndpoint + "/generate", { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(settings.proxyToken ? { Authorization: `Bearer ${settings.proxyToken}` } : {}), + }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + throw new Error(`Server responded with ${res.status}`); + } + + const data: { response?: string } = (await res.json()) as { response?: string }; + if (!data.response) { + throw new Error("No response field in server reply."); + } + aiResult = data.response; + } catch (err) { + alert("Failed to fetch AI response: " + (err as Error).message); + spinner.style.display = "none"; + sendBtn.disabled = promptEl.value.trim().length === 0; + promptEl.disabled = false; return; } const isHtml: boolean = ctx.compose.isHtml ?? Boolean(ctx.compose.bodyHTML && ctx.compose.bodyHTML.trim()); - const aiResult: string = isHtml - ? "<p>This response will be inserted by AI</p>" - : "This response will be inserted by AI"; - - const replyPrefix: string | undefined = - ctx.compose.bodyPlain?.split("\n\n")[0].trim() || undefined; const oldBody: string = isHtml ? (ctx.compose.bodyHTML ?? "") : (ctx.compose.bodyPlain ?? ""); - const newBody: string = insertAtReply(oldBody, aiResult, isHtml); await (browser as any).compose.setComposeDetails(tabId, { body: newBody }); - outputEl.textContent = JSON.stringify(payload, null, 2); + spinner.style.display = "none"; + sendBtn.disabled = promptEl.value.trim().length === 0; + promptEl.disabled = false; } +const { sendBtn, promptEl } = getElements(); sendBtn.addEventListener("click", (): void => { handleInsert().catch((err: unknown): void => { console.error("Popup insert failed:", err); - outputEl.textContent = "Error inserting text. See console."; + alert("Error inserting text. See console for details."); }); }); +promptEl.addEventListener("input", (): void => { + sendBtn.disabled = promptEl.value.trim().length === 0; +}); |
