diff --git a/src/action.ts b/src/action.ts new file mode 100644 index 0000000..6a31c0b --- /dev/null +++ b/src/action.ts @@ -0,0 +1,48 @@ +import { State } from "./state"; + +export const replaceSelectedText = ( + state: State, + parameters: { newText: string | string[] }, +): void => { + const { newText } = parameters; + const selection = state.currentSelection; + if (!newText || !selection) { + return; + } + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + range.deleteContents(); + if (Array.isArray(newText)) { + const fragment = document.createDocumentFragment(); + newText.forEach((text) => + fragment.appendChild(document.createTextNode(text)), + ); + range.insertNode(fragment); + } else { + range.insertNode(document.createTextNode(newText)); + } + selection.removeAllRanges(); + } +}; + +export const appendTextToDocument = ( + state: State, + parameters: { text: string }, +): void => { + if (window.location.hostname.includes("overleaf.com")) { + const { text } = parameters; + const editorElement = document.querySelector(".cm-content"); + if (editorElement) { + const textNode = document.createTextNode(text); + editorElement.appendChild(textNode); + + // Scroll to bottom + const scroller = document.querySelector(".cm-scroller"); + if (scroller) { + scroller.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); + } + } + } else { + throw new Error("Not Implemented"); + } +}; diff --git a/src/index.ts b/src/index.ts index f87efb5..a1eccdd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,35 +1,4 @@ -import * as Overleaf from "./page/overleaf"; -import { ToolName } from "./tool"; -export { getToolInfo, getToolsInfo, allTools } from "./tool"; - -const PAGE_HANDLER_MAP: Record = { - "www.overleaf.com": Overleaf.PageHandler, -}; - -const PAGE_TOOLS_MAP: Record = { - "www.overleaf.com": Overleaf.availableTools, -}; - -export const isPageSupported = (url: string) => { - return Object.keys(PAGE_HANDLER_MAP).includes(url); -}; - -export const isCurrentPageSupported = () => { - return Object.keys(PAGE_HANDLER_MAP).includes(window.location.hostname); -}; - -export const initHandler = () => { - if (isCurrentPageSupported()) { - return new PAGE_HANDLER_MAP[window.location.hostname](); - } - console.error("[Web Agent Interface] No tools found for the current page"); -}; - -export const getAvailableTools = () => { - if (isCurrentPageSupported()) { - return PAGE_TOOLS_MAP[window.location.hostname]; - } - console.error("[Web Agent Interface] No tools found for the current page"); -}; - -export * as Overleaf from "./page/overleaf"; +export { tool, toolName, ToolName } from "./tool"; +export { State } from "./state"; +export * as retriever from "./retriever"; +export * as action from "./action"; diff --git a/src/page/googleCalendar/commands/run_view_events.ts b/src/page/googleCalendar/commands/run_view_events.ts deleted file mode 100644 index 1fdad2f..0000000 --- a/src/page/googleCalendar/commands/run_view_events.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { calendar_v3 } from "googleapis"; -import type { JWT } from "googleapis-common"; - -type RunViewEventParams = { - auth: JWT; - calendarId: string; -}; - -const runViewEvents = async ({ auth, calendarId }: RunViewEventParams) => { - const calendar = new calendar_v3.Calendar({}); - - try { - const response = await calendar.events.list({ - auth, - calendarId, - }); - - const curatedItems = - response.data && response.data.items - ? response.data.items.map( - ({ - status, - summary, - description, - start, - end, - }: // eslint-disable-next-line @typescript-eslint/no-explicit-any - any) => ({ - status, - summary, - description, - start, - end, - }), - ) - : []; - - return `Result for view events command: \n${JSON.stringify( - curatedItems, - null, - 2, - )}`; - } catch (error) { - return `An error occurred: ${error}`; - } -}; - -export { runViewEvents }; diff --git a/src/page/googleCalendar/googleCalendar.ts b/src/page/googleCalendar/googleCalendar.ts deleted file mode 100644 index fdbc903..0000000 --- a/src/page/googleCalendar/googleCalendar.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { IPageHandler } from "../interface"; -import { google } from "googleapis"; -import { GoogleCalendarParams } from "./googleCalendar_base"; -import { runViewEvents } from "./commands/run_view_events"; - -export class PageHandler implements IPageHandler { - protected clientEmail: string; - protected privateKey: string; - protected scopes: string[]; - - constructor(fields: GoogleCalendarParams) { - this.clientEmail = fields.credentials?.clientEmail || ""; - this.privateKey = fields.credentials?.privateKey || ""; - this.scopes = fields.scopes || [ - "https://www.googleapis.com/auth/calendar", - "https://www.googleapis.com/auth/calendar.events", - ]; - } - - public getAuth = async () => { - const auth = new google.auth.JWT( - this.clientEmail, - undefined, - this.privateKey, - this.scopes, - ); - return auth; - }; - - public googleCalendarGetIDsImpl = async () => { - const auth = await this.getAuth(); - const calendar = google.calendar({ version: "v3", auth }); - const calendarList = await calendar.calendarList.list(); - const calendarIds = calendarList.data.items?.map((item) => item.id); - return calendarIds; - }; - - public googleCalendarViewEventsImpl = async (calendarId: string) => { - const auth = await this.getAuth(); - return runViewEvents({ - auth, - calendarId: calendarId, - }); - }; - - public handleToolCall(toolName: string, params: any): any { - if (toolName in this.toolImplementations) { - const toolImplementation = this.toolImplementations[toolName as ToolName]; - return toolImplementation(params); - } else { - throw new Error(`Tool '${toolName}' not found in handler.`); - } - } - - toolImplementations: Record any> = { - googleCalendarGetIDs: this.googleCalendarGetIDsImpl, - googleCalendarViewEvents: this.googleCalendarViewEventsImpl, - }; -} -export const tools = { - googleCalendarGetIDs: { - displayName: "Google Calendar Get IDs", - description: "A tool for retrieving Google Calendar IDs.", - schema: { - type: "function", - function: { - name: "googleCalendarGetIDs", - description: - "googleCalendarGetIDs() -> str - Get user's Google Calendar IDs, no parameter is needed.\\n\\n Returns:\\n calendar IDs", - parameters: { type: "object", properties: {}, required: [] }, - }, - }, - }, - googleCalendarViewEvents: { - displayName: "Google Calendar View Events", - description: "A tool for retrieving Google Calendar events and meetings.", - schema: { - type: "function", - function: { - name: "googleCalendarViewEvents", - description: - "googleCalendarViewEvents() -> str - Get the user's Google Calendar events and meetings, parameter is the calendar ID.\\n\\n Args:\\n calendarId (str): The calendar ID.\\n\\n Returns:\\n title, start time, end time, attendees, description (if available)", - parameters: { - type: "object", - properties: { - calendarId: { type: "string" }, - }, - required: ["calendarId"], - }, - }, - }, - }, -}; - -export type ToolName = keyof typeof tools; - -export function getToolInfo(): { name: ToolName; displayName: string }[] { - return Object.entries(tools).map(([name, tool]) => ({ - name: name as ToolName, - displayName: tool.displayName, - })); -} diff --git a/src/page/googleCalendar/googleCalendar_base.ts b/src/page/googleCalendar/googleCalendar_base.ts deleted file mode 100644 index bbf4f64..0000000 --- a/src/page/googleCalendar/googleCalendar_base.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface GoogleCalendarParams { - credentials?: { - clientEmail?: string; - privateKey?: string; - }; - scopes?: string[]; -} diff --git a/src/page/interface.ts b/src/page/interface.ts deleted file mode 100644 index 2f09097..0000000 --- a/src/page/interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IPageHandler { - handleToolCall(toolName: string, params: any): any; -} diff --git a/src/page/overleaf.ts b/src/page/overleaf.ts deleted file mode 100644 index 10099a6..0000000 --- a/src/page/overleaf.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { IPageHandler } from "./interface"; -import { ToolName } from "../tool"; - -/** - * Implementation of getTextSelection, replaceSelectedText, etc for Overleaf - * @class OverleafPage - */ -export class PageHandler implements IPageHandler { - public currentSelection: Selection | null = null; - - constructor() { - document.addEventListener("selectionchange", this.handleSelectionChange); - } - - public handleSelectionChange = (): void => { - const selection = window.getSelection(); - - if ( - selection && - typeof selection.rangeCount !== "undefined" && - selection.rangeCount > 0 - ) { - this.currentSelection = selection; - } - }; - - public getSelectionImpl = (): string => { - if (!this.currentSelection) { - return ""; - } - return this.currentSelection.toString(); - }; - - public replaceSelectionImpl = (params: { - newText: string | string[]; - }): void => { - const { newText } = params; - const selection = this.currentSelection; - if (!newText || !selection) { - return; - } - if (selection.rangeCount > 0) { - const range = selection.getRangeAt(0); - range.deleteContents(); - if (Array.isArray(newText)) { - const fragment = document.createDocumentFragment(); - newText.forEach((text) => - fragment.appendChild(document.createTextNode(text)), - ); - range.insertNode(fragment); - } else { - range.insertNode(document.createTextNode(newText)); - } - selection.removeAllRanges(); - } - }; - - public appendTextImpl = (params: { text: string }): void => { - const { text } = params; - const editorElement = document.querySelector(".cm-content"); - if (editorElement) { - const textNode = document.createTextNode(text); - editorElement.appendChild(textNode); - - // Scroll to bottom - const scroller = document.querySelector(".cm-scroller"); - if (scroller) { - scroller.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); - } - } - }; - - public handleToolCall(toolName: string, params: any): any { - if (toolName in this.toolImplementations) { - const toolImplementation = this.toolImplementations[toolName as ToolName]; - return toolImplementation(params); - } else { - throw new Error(`Tool '${toolName}' is not available on this page.`); - } - } - - toolImplementations: Record any> = { - getSelectedText: this.getSelectionImpl, - replaceSelectedText: this.replaceSelectionImpl, - appendTextToDocument: this.appendTextImpl, - }; -} - -export const availableTools: ToolName[] = [ - "getSelectedText", - "replaceSelectedText", - "appendTextToDocument", -]; diff --git a/src/retriever.ts b/src/retriever.ts new file mode 100644 index 0000000..d3432ae --- /dev/null +++ b/src/retriever.ts @@ -0,0 +1,8 @@ +import { State } from "./state"; + +export const getSelectedText = (state: State): string => { + if (!state.currentSelection) { + return ""; + } + return state.currentSelection.toString(); +}; diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..0d93f66 --- /dev/null +++ b/src/state.ts @@ -0,0 +1,17 @@ +export class State { + public currentSelection: Selection | undefined; + + constructor() { + document.addEventListener("selectionchange", (): void => { + const selection = window.getSelection(); + + if ( + selection && + typeof selection.rangeCount !== "undefined" && + selection.rangeCount > 0 + ) { + this.currentSelection = selection; + } + }); + } +} diff --git a/src/tool.ts b/src/tool.ts index 6eadd42..0820b6b 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -1,4 +1,8 @@ -const tools: Record = { +import { appendTextToDocument, replaceSelectedText } from "./action"; +import { getSelectedText } from "./retriever"; +import { State } from "./state"; + +export const tool: Record = { getSelectedText: { name: "getSelectedText", displayName: "Get Selected Text", @@ -13,6 +17,7 @@ const tools: Record = { parameters: { type: "object", properties: {}, required: [] }, }, }, + implementation: getSelectedText, }, replaceSelectedText: { name: "replaceSelectedText", @@ -34,6 +39,7 @@ const tools: Record = { }, }, }, + implementation: replaceSelectedText, }, appendTextToDocument: { name: "appendTextToDocument", @@ -54,24 +60,30 @@ const tools: Record = { }, }, }, + implementation: appendTextToDocument, }, }; -export const allTools = Object.keys(tools); - -export type ToolName = keyof typeof tools; +export const toolName = Object.keys(tool); +export type ToolName = keyof typeof tool; -export interface ToolInfo { +export interface Tool { name: ToolName; displayName: string; description: string; - schema: any; -} - -export function getToolsInfo(): Record { - return tools; -} - -export function getToolInfo(name: ToolName): ToolInfo { - return tools[name]; + schema: { + type: "function"; + function: { + name: string; + description: string; + parameters: { + type: "object"; + properties: Record; + required: Array< + keyof Tool["schema"]["function"]["parameters"]["properties"] + >; + }; + }; + }; + implementation: (state: State, parameters: any) => void; }