Bug 1902449 - Rework chat to support contextual prompts and non-contextmenu entry points r=tarek

Refactor building chat menu from prefs and context passing. Reuse message targeting for optional contextual filtering of prompts.

Differential Revision: https://phabricator.services.mozilla.com/D214332
This commit is contained in:
Ed Lee 2024-06-20 14:39:24 +00:00
parent d651d07b94
commit d51f63c6dc
6 changed files with 118 additions and 38 deletions

View File

@ -1905,7 +1905,7 @@ pref("sidebar.main.tools", "history,syncedtabs");
pref("sidebar.verticalTabs", false);
pref("browser.ml.chat.enabled", false);
pref("browser.ml.chat.prompt.prefix", 'Im on page "%currentTabTitle%" with "%selection|12000%" selected. ');
pref("browser.ml.chat.prompt.prefix", 'Im on page "%tabTitle%" with "%selection|12000%" selected. ');
pref("browser.ml.chat.prompts.0", '{"label":"Summarize","value":"Please summarize the selection using precise and concise language. Highlight the main themes and conclusions. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning of the selection."}');
pref("browser.ml.chat.prompts.1", '{"label":"Simplify language","value":"Please rewrite the selection in plain, clear language suitable for a general audience without specialized knowledge. Use all of the following tactics: simple vocabulary; short sentences; active voice; examples where applicable to make explanations clearer; explanations for jargon and technical terms; headers and bulleted lists for scannability. Maintain factual accuracy while simplifying."}');
pref("browser.ml.chat.prompts.2", '{"label":"Quiz me","value":"Please create questions related to the selection. Ask the questions one by one. Wait for my response before moving on to the next question. Evaluate each response. Ask a variety of types of questions, like multiple choice, true or false and short answer."}');

View File

@ -238,9 +238,6 @@ document.addEventListener(
case "context-translate-selection":
gContextMenu.openSelectTranslationsPanel(event);
break;
case "context-ask-chat":
nsContextMenu.GenAI.handleAskChat(event);
break;
case "context-showonlythisframe":
gContextMenu.showOnlyThisFrame();
break;

View File

@ -7,6 +7,9 @@
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ASRouterTargeting: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"chatEnabled",
@ -53,32 +56,67 @@ export const GenAI = {
/**
* Build prompts menu to ask chat for context menu or popup.
*
* @param {MozMenu} menu Element to update
* @param {nsContextMenu} context Additional menu context
* @param {MozMenu} menu element to update
* @param {nsContextMenu} nsContextMenu helpers for context menu
*/
buildAskChatMenu(menu, context) {
async buildAskChatMenu(menu, nsContextMenu) {
nsContextMenu.showItem(menu, false);
if (!lazy.chatEnabled || lazy.chatProvider == "") {
context.showItem(menu, false);
return;
}
menu.context = context;
menu.label = "Ask chatbot";
menu.menupopup?.remove();
// Prepare context used for both targeting and handling prompts
const window = menu.ownerGlobal;
const tab = window.gBrowser.getTabForBrowser(nsContextMenu.browser);
const context = {
provider: lazy.chatProvider,
selection: nsContextMenu.selectionInfo.fullText ?? "",
tabTitle: (tab._labelIsContentTitle && tab.label) || "",
window,
};
// Add menu items that pass along context for handling
(await this.getContextualPrompts(context)).forEach(promptObj =>
menu
.appendItem(promptObj.label, promptObj.value)
.addEventListener("command", () =>
this.handleAskChat(promptObj, context)
)
);
nsContextMenu.showItem(menu, menu.itemCount > 0);
},
/**
* Get prompts from prefs evaluated with context
*
* @param {object} context data used for targeting
* @returns {promise} array of matching prompt objects
*/
getContextualPrompts(context) {
// Treat prompt objects as messages to reuse targeting capabilities
const messages = [];
Services.prefs.getChildList("browser.ml.chat.prompts.").forEach(pref => {
try {
let prompt = Services.prefs.getStringPref(pref);
const promptObj = {
label: Services.prefs.getStringPref(pref),
value: "",
};
try {
prompt = JSON.parse(prompt);
// Prompts can be JSON with label, value, targeting and other keys
Object.assign(promptObj, JSON.parse(promptObj.label));
} catch (ex) {}
menu
.appendItem(prompt.label ?? prompt, prompt.value ?? "")
.addEventListener("command", this.handleAskChat.bind(this));
messages.push(promptObj);
} catch (ex) {
console.error("Failed to add menu item for " + pref, ex);
console.error("Failed to get prompt pref " + pref, ex);
}
});
context.showItem(menu, menu.itemCount > 0);
return lazy.ASRouterTargeting.findMatchingMessage({
messages,
returnAll: true,
trigger: { context },
});
},
/**
@ -104,18 +142,11 @@ export const GenAI = {
/**
* Handle selected prompt by opening tab or sidebar.
*
* @param {Event} event from menu command
* @param {object} promptObj to convert to string
* @param {object} context of how the prompt should be handled
*/
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 ?? "",
});
async handleAskChat(promptObj, context) {
const prompt = this.buildChatPrompt(promptObj, context);
// Pass the prompt via GET url ?q= param or request header
const { header } = this.chatProviders.get(lazy.chatProvider) ?? {};
@ -139,10 +170,11 @@ export const GenAI = {
// Get the desired browser to handle the prompt url request
let browser;
if (lazy.chatSidebar) {
const { SidebarController } = context.window;
await SidebarController.show("viewGenaiChatSidebar");
browser = await SidebarController.browser.contentWindow.browserPromise;
} else {
browser = gBrowser.addTab("", options).linkedBrowser;
browser = context.window.gBrowser.addTab("", options).linkedBrowser;
}
browser.fixupAndLoadURIString(url, options);
},

View File

@ -26,23 +26,16 @@ add_task(async function test_chat_header() {
sandbox
.stub(GenAI, "chatProviders")
.value(new Map([[url, { header: "X-Prompt" }]]));
sandbox.stub(GenAI, "buildChatPrompt").returns("hello world?");
await SpecialPowers.pushPrefEnv({
set: [
["browser.ml.chat.provider", url],
["browser.ml.chat.prompt.prefix", ""],
["browser.ml.chat.sidebar", false],
],
});
await GenAI.handleAskChat({
target: {
closest() {
return { context: { selectionInfo: {} } };
},
ownerGlobal: window,
},
});
await GenAI.handleAskChat({ value: "hello world?" }, { window });
const request = await requestPromise;
Assert.equal(
request.getHeader("x-prompt"),

View File

@ -0,0 +1,57 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const { GenAI } = ChromeUtils.importESModule(
"resource:///modules/GenAI.sys.mjs"
);
registerCleanupFunction(() =>
Services.prefs.clearUserPref("browser.ml.chat.prompts.test")
);
/**
* Check prompts from prefs are used
*/
add_task(async function test_prompt_prefs() {
const origPrompts = (await GenAI.getContextualPrompts()).length;
Services.prefs.setStringPref("browser.ml.chat.prompts.test", "{}");
Assert.equal(
(await GenAI.getContextualPrompts()).length,
origPrompts + 1,
"Added a prompt"
);
Services.prefs.setStringPref(
"browser.ml.chat.prompts.test",
JSON.stringify({ targeting: "false" })
);
Assert.equal(
(await GenAI.getContextualPrompts()).length,
origPrompts,
"Prompt not added for targeting"
);
});
/**
* Check context is used for targeting
*/
add_task(async function test_prompt_context() {
Services.prefs.setStringPref(
"browser.ml.chat.prompts.test",
JSON.stringify({ targeting: "provider" })
);
const origPrompts = (await GenAI.getContextualPrompts()).length;
Assert.equal(
(await GenAI.getContextualPrompts({ provider: "localhost" })).length,
origPrompts + 1,
"Added contextual prompt"
);
Assert.equal(
(await GenAI.getContextualPrompts({ provider: "" })).length,
origPrompts,
"Prompt not added for contextual targeting"
);
});

View File

@ -2,3 +2,4 @@
firefox-appdir = "browser"
["test_build_chat_prompt.js"]
["test_contextual_prompts.js"]