187 lines
5.5 KiB
JavaScript
187 lines
5.5 KiB
JavaScript
/**
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"chatEnabled",
|
|
"browser.ml.chat.enabled"
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"chatOpenSidebarOnProviderChange",
|
|
"browser.ml.chat.openSidebarOnProviderChange",
|
|
true
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"chatPromptPrefix",
|
|
"browser.ml.chat.prompt.prefix"
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"chatProvider",
|
|
"browser.ml.chat.provider",
|
|
null,
|
|
(_pref, _old, val) => onChatProviderChange(val)
|
|
);
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"chatSidebar",
|
|
"browser.ml.chat.sidebar"
|
|
);
|
|
|
|
export const GenAI = {
|
|
chatProviders: new Map(),
|
|
|
|
/**
|
|
* Handle startup tasks like telemetry, adding listeners.
|
|
*/
|
|
init() {
|
|
// Access this getter for its side effect of observing provider pref change
|
|
lazy.chatProvider;
|
|
|
|
// Detect about:preferences to add controls
|
|
Services.obs.addObserver(this, "experimental-pane-loaded");
|
|
},
|
|
|
|
/**
|
|
* Build prompts menu to ask chat for context menu or popup.
|
|
*
|
|
* @param {MozMenu} menu Element to update
|
|
* @param {nsContextMenu} context Additional menu context
|
|
*/
|
|
buildAskChatMenu(menu, context) {
|
|
if (!lazy.chatEnabled || lazy.chatProvider == "") {
|
|
context.showItem(menu, false);
|
|
return;
|
|
}
|
|
|
|
menu.context = context;
|
|
menu.label = "Ask chatbot";
|
|
menu.menupopup?.remove();
|
|
Services.prefs.getChildList("browser.ml.chat.prompts.").forEach(pref => {
|
|
try {
|
|
let prompt = Services.prefs.getStringPref(pref);
|
|
try {
|
|
prompt = JSON.parse(prompt);
|
|
} catch (ex) {}
|
|
menu
|
|
.appendItem(prompt.label ?? prompt, prompt.value ?? "")
|
|
.addEventListener("command", this.handleAskChat.bind(this));
|
|
} catch (ex) {
|
|
console.error("Failed to add menu item for " + pref, ex);
|
|
}
|
|
});
|
|
context.showItem(menu, menu.itemCount > 0);
|
|
},
|
|
|
|
/**
|
|
* Build a prompt with context.
|
|
*
|
|
* @param {MozMenuItem} item Use value falling back to label
|
|
* @param {object} context Placeholder keys with values to replace
|
|
* @returns {string} Prompt with placeholders replaced
|
|
*/
|
|
buildChatPrompt(item, context = {}) {
|
|
// Combine prompt prefix with the item then replace placeholders from the
|
|
// original prompt (and not from context)
|
|
return (lazy.chatPromptPrefix + (item.value || item.label)).replace(
|
|
// Handle %placeholder% as key|options
|
|
/\%(\w+)(?:\|([^%]+))?\%/g,
|
|
(placeholder, key, options) =>
|
|
// Currently only supporting numeric options for slice with `undefined`
|
|
// resulting in whole string
|
|
context[key]?.slice(0, options) ?? placeholder
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Handle selected prompt by opening tab or sidebar.
|
|
*
|
|
* @param {Event} event from menu command
|
|
*/
|
|
async handleAskChat({ target }) {
|
|
// TODO bug 1902449 to make this less context-menu specific
|
|
const win = target.ownerGlobal;
|
|
const { gBrowser, SidebarController } = win;
|
|
const { selectedTab } = gBrowser;
|
|
const prompt = this.buildChatPrompt(target, {
|
|
currentTabTitle:
|
|
(selectedTab._labelIsContentTitle && selectedTab.label) || "",
|
|
selection: target.closest("menu").context.selectionInfo.fullText ?? "",
|
|
});
|
|
|
|
// Pass the prompt via GET url ?q= param or request header
|
|
const { header } = this.chatProviders.get(lazy.chatProvider) ?? {};
|
|
const url = new URL(lazy.chatProvider);
|
|
const options = {
|
|
inBackground: false,
|
|
relatedToCurrent: true,
|
|
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
|
|
{}
|
|
),
|
|
};
|
|
if (header) {
|
|
options.headers = Cc[
|
|
"@mozilla.org/io/string-input-stream;1"
|
|
].createInstance(Ci.nsIStringInputStream);
|
|
options.headers.data = `${header}: ${encodeURIComponent(prompt)}\r\n`;
|
|
} else {
|
|
url.searchParams.set("q", prompt);
|
|
}
|
|
|
|
// Get the desired browser to handle the prompt url request
|
|
let browser;
|
|
if (lazy.chatSidebar) {
|
|
await SidebarController.show("viewGenaiChatSidebar");
|
|
browser = await SidebarController.browser.contentWindow.browserPromise;
|
|
} else {
|
|
browser = gBrowser.addTab("", options).linkedBrowser;
|
|
}
|
|
browser.fixupAndLoadURIString(url, options);
|
|
},
|
|
|
|
/**
|
|
* Build preferences for chat such as handling providers.
|
|
*
|
|
* @param {Window} window for about:preferences
|
|
*/
|
|
buildPreferences({ document, Preferences }) {
|
|
const providerEl = document.getElementById("genai-chat-provider");
|
|
if (!providerEl) {
|
|
return;
|
|
}
|
|
|
|
const enabled = Preferences.get("browser.ml.chat.enabled");
|
|
const onEnabledChange = () => (providerEl.disabled = !enabled.value);
|
|
onEnabledChange();
|
|
enabled.on("change", onEnabledChange);
|
|
|
|
// TODO bug 1895433 populate providers
|
|
Preferences.add({ id: "browser.ml.chat.provider", type: "string" });
|
|
},
|
|
|
|
// nsIObserver
|
|
observe(window) {
|
|
this.buildPreferences(window);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Ensure the chat sidebar is shown to reflect changed provider.
|
|
*
|
|
* @param {string} value New pref value
|
|
*/
|
|
function onChatProviderChange(value) {
|
|
if (value && lazy.chatEnabled && lazy.chatOpenSidebarOnProviderChange) {
|
|
Services.wm
|
|
.getMostRecentWindow("navigator:browser")
|
|
?.SidebarController.show("viewGenaiChatSidebar");
|
|
}
|
|
}
|