diff --git a/extension/app/background.ts b/extension/app/background.ts index ad0507650efc..b6a5b67a1f08 100644 --- a/extension/app/background.ts +++ b/extension/app/background.ts @@ -15,16 +15,17 @@ import type { GetActiveTabBackgroundResponse, InputBarStatusMessage, } from "./src/lib/messages"; -import { sendAttachSelection as sendAttachSelection } from "./src/lib/messages"; import { generatePKCE } from "./src/lib/utils"; const log = console.error; const state: { + port: chrome.runtime.Port | undefined; extensionReady: boolean; inputBarReady: boolean; lastHandler: (() => void) | undefined; } = { + port: undefined, extensionReady: false, inputBarReady: false, lastHandler: undefined, @@ -46,6 +47,11 @@ chrome.runtime.onUpdateAvailable.addListener(async (details) => { */ chrome.runtime.onInstalled.addListener(() => { void chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); + chrome.contextMenus.create({ + id: "ask_dust", + title: "Ask @dust to summarize this page", + contexts: ["all"], + }); chrome.contextMenus.create({ id: "add_tab_content", title: "Add tab content to conversation", @@ -67,10 +73,12 @@ chrome.runtime.onInstalled.addListener(() => { chrome.runtime.onConnect.addListener((port) => { if (port.name === "sidepanel-connection") { console.log("Sidepanel is there"); + state.port = port; state.extensionReady = true; port.onDisconnect.addListener(() => { // This fires when sidepanel closes console.log("Sidepanel was closed"); + state.port = undefined; state.extensionReady = false; state.inputBarReady = false; state.lastHandler = undefined; @@ -80,27 +88,53 @@ chrome.runtime.onConnect.addListener((port) => { const getActionHandler = (menuItemId: string | number) => { switch (menuItemId) { + case "ask_dust": + return () => { + if (state.port) { + const params = JSON.stringify({ + includeContent: true, + includeScreenshot: false, + text: ":mention[dust]{sId=dust} summarize this page.", + configurationId: "dust", + }); + state.port.postMessage({ + type: "ROUTE_CHANGE", + pathname: "/run", + search: `?${params}`, + }); + } + }; case "add_tab_content": - return () => - void sendAttachSelection({ - includeContent: true, - includeScreenshot: false, - }); - break; + return () => { + if (state.port) { + state.port.postMessage({ + type: "ATTACH_TAB", + includeContent: true, + includeScreenshot: false, + }); + } + }; case "add_tab_screenshot": - return () => - void sendAttachSelection({ - includeContent: false, - includeScreenshot: true, - }); - break; + return () => { + if (state.port) { + state.port.postMessage({ + type: "ATTACH_TAB", + includeContent: false, + includeScreenshot: true, + }); + } + }; case "add_selection": - return () => - void sendAttachSelection({ - includeContent: true, - includeScreenshot: false, - includeSelectionOnly: true, - }); + return () => { + if (state.port) { + state.port.postMessage({ + type: "ATTACH_TAB", + includeContent: true, + includeScreenshot: false, + includeSelectionOnly: true, + }); + } + }; } }; @@ -116,10 +150,8 @@ chrome.contextMenus.onClicked.addListener(async (event, tab) => { void chrome.sidePanel.open({ windowId: tab.windowId, }); - } else if (state.inputBarReady) { - handler(); } else { - // Extension is loaded but the input bar is not visible - do nothing. + await handler(); } }); diff --git a/extension/app/main.tsx b/extension/app/main.tsx index fcdea4a8c0a4..5bf3187f168d 100644 --- a/extension/app/main.tsx +++ b/extension/app/main.tsx @@ -9,6 +9,7 @@ import "./src/css/custom.css"; import { Notification } from "@dust-tt/sparkle"; import { AuthProvider } from "@extension/components/auth/AuthProvider"; +import { PortProvider } from "@extension/components/PortContext"; import { routes } from "@extension/pages/routes"; import ReactDOM from "react-dom/client"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; @@ -16,13 +17,14 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; const router = createBrowserRouter(routes); const App = () => { - chrome.runtime.connect({ name: "sidepanel-connection" }); return ( - - - - - + + + + + + + ); }; const rootElement = document.getElementById("root"); diff --git a/extension/app/src/components/PortContext.tsx b/extension/app/src/components/PortContext.tsx new file mode 100644 index 000000000000..df4e6930685c --- /dev/null +++ b/extension/app/src/components/PortContext.tsx @@ -0,0 +1,19 @@ +import { createContext, useEffect, useState } from "react"; + +export const PortContext = createContext(null); + +export const PortProvider = ({ children }: { children: React.ReactNode }) => { + const [port, setPort] = useState(null); + + useEffect(() => { + const port = chrome.runtime.connect({ name: "sidepanel-connection" }); + + setPort(port); + + return () => { + port.disconnect(); + }; + }, []); + + return {children}; +}; diff --git a/extension/app/src/components/auth/ProtectedRoute.tsx b/extension/app/src/components/auth/ProtectedRoute.tsx index 9c53ed44f28e..fd30d1af2522 100644 --- a/extension/app/src/components/auth/ProtectedRoute.tsx +++ b/extension/app/src/components/auth/ProtectedRoute.tsx @@ -6,10 +6,12 @@ import { Spinner, } from "@dust-tt/sparkle"; import { useAuth } from "@extension/components/auth/AuthProvider"; +import { PortContext } from "@extension/components/PortContext"; +import type { RouteChangeMesssage } from "@extension/lib/messages"; import type { StoredUser } from "@extension/lib/storage"; import { getPendingUpdate } from "@extension/lib/storage"; import type { ReactNode } from "react"; -import { useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; type ProtectedRouteProps = { @@ -35,6 +37,23 @@ export const ProtectedRoute = ({ children }: ProtectedRouteProps) => { const navigate = useNavigate(); const [isLatestVersion, setIsLatestVersion] = useState(true); + const port = useContext(PortContext); + useEffect(() => { + if (port) { + const listener = (message: RouteChangeMesssage) => { + const { type } = message; + if (type === "ROUTE_CHANGE") { + navigate({ pathname: message.pathname, search: message.search }); + return false; + } + }; + port.onMessage.addListener(listener); + return () => { + port.onMessage.removeListener(listener); + }; + } + }, [port, navigate]); + useEffect(() => { if (!isAuthenticated || !isUserSetup || !user || !workspace) { navigate("/login"); diff --git a/extension/app/src/components/input_bar/InputBar.tsx b/extension/app/src/components/input_bar/InputBar.tsx index af189033c7cc..3f7bd5ab6762 100644 --- a/extension/app/src/components/input_bar/InputBar.tsx +++ b/extension/app/src/components/input_bar/InputBar.tsx @@ -13,6 +13,7 @@ import { InputBarCitations } from "@extension/components/input_bar/InputBarCitat import type { InputBarContainerProps } from "@extension/components/input_bar/InputBarContainer"; import { InputBarContainer } from "@extension/components/input_bar/InputBarContainer"; import { InputBarContext } from "@extension/components/input_bar/InputBarContext"; +import { PortContext } from "@extension/components/PortContext"; import { useFileUploaderService } from "@extension/hooks/useFileUploaderService"; import { useDustAPI } from "@extension/lib/dust_api"; import type { AttachSelectionMessage } from "@extension/lib/messages"; @@ -58,21 +59,24 @@ export function AssistantInputBar({ owner, }); + const port = useContext(PortContext); useEffect(() => { - void sendInputBarStatus(true); - const listener = (message: AttachSelectionMessage) => { - const { type, ...options } = message; - if (type === "ATTACH_TAB") { - // Handle message - void fileUploaderService.uploadContentTab(options); - } - }; - chrome.runtime.onMessage.addListener(listener); - return () => { - void sendInputBarStatus(false); - chrome.runtime.onMessage.removeListener(listener); - }; - }); + if (port) { + void sendInputBarStatus(true); + const listener = async (message: AttachSelectionMessage) => { + const { type } = message; + if (type === "ATTACH_TAB") { + // Handle message + void fileUploaderService.uploadContentTab(message); + } + }; + port.onMessage.addListener(listener); + return () => { + void sendInputBarStatus(false); + port.onMessage.removeListener(listener); + }; + } + }, []); const { droppedFiles, setDroppedFiles } = useFileDrop(); diff --git a/extension/app/src/lib/messages.ts b/extension/app/src/lib/messages.ts index fe3db40e8cf1..06ed4d8eae3c 100644 --- a/extension/app/src/lib/messages.ts +++ b/extension/app/src/lib/messages.ts @@ -41,6 +41,12 @@ export type InputBarStatusMessage = { available: boolean; }; +export type RouteChangeMesssage = { + type: "ROUTE_CHANGE"; + pathname: string; + search: string; +}; + const sendMessage = (message: T): Promise => { return new Promise((resolve, reject) => { chrome.runtime.sendMessage(message, (response: U | undefined) => { @@ -147,6 +153,15 @@ export const sendGetActiveTabMessage = (params: GetActiveTabOptions) => { }); }; +export const sendInputBarStatus = (available: boolean) => { + return sendMessage({ + type: "INPUT_BAR_STATUS", + available, + }); +}; + +// Messages from background script to content script + export const sendAttachSelection = ( opts: GetActiveTabOptions = { includeContent: true, includeScreenshot: false } ) => { @@ -155,10 +170,3 @@ export const sendAttachSelection = ( ...opts, }); }; - -export const sendInputBarStatus = (available: boolean) => { - return sendMessage({ - type: "INPUT_BAR_STATUS", - available, - }); -}; diff --git a/extension/app/src/pages/RunPage.tsx b/extension/app/src/pages/RunPage.tsx new file mode 100644 index 000000000000..14c838a6a6ae --- /dev/null +++ b/extension/app/src/pages/RunPage.tsx @@ -0,0 +1,60 @@ +import type { LightWorkspaceType } from "@dust-tt/client"; +import { Spinner } from "@dust-tt/sparkle"; +import { useFileUploaderService } from "@extension/hooks/useFileUploaderService"; +import { postConversation } from "@extension/lib/conversation"; +import { useDustAPI } from "@extension/lib/dust_api"; +import { useEffect } from "react"; +import { useLocation, useNavigate } from "react-router"; + +export const RunPage = ({ workspace }: { workspace: LightWorkspaceType }) => { + const navigate = useNavigate(); + const location = useLocation(); + const dustAPI = useDustAPI(); + + const fileUploaderService = useFileUploaderService({ + owner: workspace, + }); + + useEffect(() => { + const run = async () => { + const params = JSON.parse(decodeURI(location.search.substr(1))); + + const files = await fileUploaderService.uploadContentTab({ + includeContent: params.includeContent, + includeScreenshot: params.includeScreenshot, + includeSelectionOnly: params.includeSelectionOnly, + updateBlobs: false, + }); + + const conversationRes = await postConversation({ + dustAPI, + messageData: { + input: params.text, + mentions: [{ configurationId: params.configurationId }], + }, + contentFragments: files + ? files.map((cf) => ({ + title: cf.filename, + fileId: cf.fileId || "", + url: cf.publicUrl, + })) + : [], + }); + + fileUploaderService.resetUpload(); + + if (conversationRes.isOk()) { + navigate(`/conversations/${conversationRes.value.sId}`); + } else { + navigate("/"); + } + }; + + void run(); + }, []); + return ( +
+ +
+ ); +}; diff --git a/extension/app/src/pages/routes.tsx b/extension/app/src/pages/routes.tsx index 7e7ccca72614..6e022b7ae450 100644 --- a/extension/app/src/pages/routes.tsx +++ b/extension/app/src/pages/routes.tsx @@ -3,6 +3,7 @@ import { ConversationPage } from "@extension/pages/ConversationPage"; import { ConversationsPage } from "@extension/pages/ConversationsPage"; import { LoginPage } from "@extension/pages/LoginPage"; import { MainPage } from "@extension/pages/MainPage"; +import { RunPage } from "@extension/pages/RunPage"; export const routes = [ { @@ -51,4 +52,12 @@ export const routes = [ ), }, + { + path: "/run", + element: ( + + {({ workspace }) => } + + ), + }, ];