firefox-desktop/browser/components/genai/GenAI.sys.mjs
2024-06-20 20:47:58 +02:00

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");
}
}