From 62dd8a006a3ebc9d00314d2a114b25b436d11171 Mon Sep 17 00:00:00 2001 From: Aiman Kandakji Date: Sat, 21 Jun 2025 21:23:24 +0300 Subject: [PATCH 1/2] feat(sheets): add Google Sheets automation support - Implement new services for sheet classification and automation - Add hooks for sheet detection, metadata extraction and automation execution - Create Google Sheets API service for data and chart operations - Extend automation runner to handle both forms and sheets workflows - Remove unused color properties from notification constants --- .../components/page/home/NotificationItem.tsx | 2 +- client/src/constants/constants.ts | 3 +- client/src/hooks/useAutomationRunner.ts | 111 ++++------ client/src/hooks/useSheetAutomation.ts | 93 +++++++++ client/src/hooks/useSheetDetector.ts | 43 ++++ client/src/hooks/useSheetMetadata.ts | 195 ++++++++++++++++++ client/src/lib/services/sheets.ts | 84 ++++++++ client/src/pages/Home.tsx | 1 - client/src/types/sheets.ts | 31 +++ server/app.js | 2 + server/routes/sheets.js | 32 +++ server/services/openai.js | 64 +++++- server/services/sheetAutomation.js | 32 +++ server/services/sheetClassifier.js | 23 +++ 14 files changed, 637 insertions(+), 79 deletions(-) create mode 100644 client/src/hooks/useSheetAutomation.ts create mode 100644 client/src/hooks/useSheetDetector.ts create mode 100644 client/src/hooks/useSheetMetadata.ts create mode 100644 client/src/lib/services/sheets.ts create mode 100644 client/src/types/sheets.ts create mode 100644 server/routes/sheets.js create mode 100644 server/services/sheetAutomation.js create mode 100644 server/services/sheetClassifier.js diff --git a/client/src/components/page/home/NotificationItem.tsx b/client/src/components/page/home/NotificationItem.tsx index 5ca4266..eb81c46 100644 --- a/client/src/components/page/home/NotificationItem.tsx +++ b/client/src/components/page/home/NotificationItem.tsx @@ -3,7 +3,7 @@ export interface Item { name: string; description: string; icon: string; - color: string; + color?: string; time: string; } const NotificationItem = ({ name, description, icon, color, time }: Item) => { diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index 5813753..a05fb8c 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -61,7 +61,6 @@ export const AUTOMATION_ERROR_NOTIFICATION: Item = { description: "Something went wrong during automation.", time: "now", icon: "⚠️", - color: "#ef4444", // red-500 }; export const INITIAL_NOTIFICATION_MESSAGE = "Waking up the bots..."; export const WAITING_FOR_AI_MESSAGE = "AI is deep in thought..."; @@ -99,7 +98,7 @@ export const INITIAL_NOTIFICATIONS: Omit[] = [ export const SUCCESS_NOTIFICATION_BASE: Omit = { name: "SUCCESS", icon: "✅", - color: "#10B981", + // color: "#10B981", }; export const ERROR_NOTIFICATION_BASE: Omit = { diff --git a/client/src/hooks/useAutomationRunner.ts b/client/src/hooks/useAutomationRunner.ts index 8b96e50..eff3528 100644 --- a/client/src/hooks/useAutomationRunner.ts +++ b/client/src/hooks/useAutomationRunner.ts @@ -1,12 +1,13 @@ -// ... existing code ... import { AuthContext } from "@/contexts/AuthContext"; import { useContext, useState } from "react"; import { useAiFormFiller } from "./useAiFormFiller"; -// import { useAutomationNotifications } from "./useAutomationNotifications"; // Removed import { useFormInjector } from "./useFormInjector"; import { useFormStructurer } from "./useFormStructurer"; import { useHtmlExtractor } from "./useHtmlExtractor"; -import { Item as NotificationItemProps } from "@/components/page/home/NotificationItem"; // Added for type safety +import { Item as NotificationItemProps } from "@/components/page/home/NotificationItem"; +import { useSheetDetector } from "./useSheetDetector"; +import { useSheetAutomation } from "./useSheetAutomation"; +import { useSheetMetadata } from "./useSheetMetadata"; // Define the props for notification handlers interface AutomationRunnerProps { @@ -16,7 +17,7 @@ interface AutomationRunnerProps { onAuthError: () => void; onSuccess: () => void; onError: (message?: string) => void; - onAddNotification: (message: string, type: 'add' | 'replace') => void; // Updated signature + onAddNotification: (message: string, type: "add" | "replace") => void; // Updated signature onReplaceNotification: (notification: NotificationItemProps) => void; onClearNotifications: () => void; } @@ -50,86 +51,62 @@ export const useAutomationRunner = (props: AutomationRunnerProps) => { const auth = useContext(AuthContext); const [isRunning, setIsRunning] = useState(false); - - // Removed internal useAutomationNotifications call - const { extractHtml } = useHtmlExtractor(); const { structureForm } = useFormStructurer(); const { sendToAI } = useAiFormFiller(); const { injectFields } = useFormInjector(); + const { isGoogleSheet } = useSheetDetector(); + const { classifyIntent, executeAutomation } = useSheetAutomation(); + const { extractMetadata } = useSheetMetadata(); const runAutomation = async (userInput: string) => { - // Input validation is now handled in Home.tsx before calling runAutomation - // Auth check if (!auth || !auth.token) { console.log("Authentication Error: User not logged in or token expired."); - onAuthError(); // Call the handler from Home.tsx + onAuthError(); return; } setIsRunning(true); - // Clear any previous notifications and schedule initial ones via Home.tsx callbacks - // onClearNotifications(); // Home.tsx's handleAutomationClick already clears. - // The FORM_FILLING_NOTIFICATIONS constant is defined in Home.tsx and passed to onScheduleInitialNotifications there. - // This call is now made from Home.tsx before runAutomation, or as the first step here if preferred. - // For now, assuming Home.tsx calls its `scheduleNotifications(INITIAL_AUTOMATION_NOTIFICATIONS)` - // which in turn calls the `onScheduleInitialNotifications` passed to this hook. - // To be very explicit as per user request: - // onScheduleInitialNotifications should be called by Home.tsx, which then calls the function passed here. - // If Home.tsx passes its own `scheduleNotifications` as `onScheduleInitialNotifications`, that's fine. - // The key is that the initial sequence starts immediately. - // Let's assume Home.tsx's `useAutomationRunner` call looks like: - // useAutomationRunner({ onScheduleInitialNotifications: () => scheduleNotifications(INITIAL_AUTOMATION_NOTIFICATIONS), ... }) - // So, when runAutomation is called, Home.tsx has already (or will immediately) call its local scheduleNotifications. - // To ensure it's the *very first* thing as per user request, Home.tsx should call its local `scheduleNotifications` - // *before* calling `runAutomation(formDataInput)`. - // The `onScheduleInitialNotifications` prop is more for the hook to trigger it if it were managing the constants. - // Given Home.tsx now owns the constants, it's more direct for Home.tsx to initiate this. - - // However, to strictly follow the user's checklist for the hook: - // If `FORM_FILLING_NOTIFICATIONS` were defined here or passed in: - // onScheduleInitialNotifications(FORM_FILLING_NOTIFICATIONS_FROM_SOMEWHERE); - // Since Home.tsx defines them, it's cleaner for Home.tsx to call its own `scheduleNotifications` - // and pass that function as `onScheduleInitialNotifications`. - // The current setup in the previous Home.tsx diff already does this effectively. - try { - console.log("Starting automation..."); - - // 1. HTML Extraction - console.log("Step 1: Extracting HTML..."); - // Notification for this step is part of the initial scheduled sequence from Home.tsx - const htmlContent = await extractHtml(); - console.log("HTML extracted successfully."); - - // 2. Structuring Form - console.log("Step 2: Structuring form data..."); - // Notification for this step is part of the initial scheduled sequence from Home.tsx - const structuredForm = await structureForm(htmlContent); - console.log("Form data structured successfully."); - - // 3. Sending to AI - console.log("Step 3: Sending data to AI..."); - // Notification for this step is part of the initial scheduled sequence from Home.tsx - const aiData = await sendToAI(userInput, structuredForm); - console.log("AI response received."); - onClearNotifications(); - // Pass description as message, and type as 'replace' or 'add' - // Assuming AI_RESPONSE_RECEIVED_NOTIFICATION should replace the last one (e.g., "Waiting for AI") - onAddNotification(AI_RESPONSE_RECEIVED_NOTIFICATION.description, 'replace'); - - console.log("Step 4: Injecting form values..."); - // Assuming INJECTING_FORM_VALUES_NOTIFICATION should be added as a new one - onAddNotification(INJECTING_FORM_VALUES_NOTIFICATION.description, 'add'); - await injectFields(aiData); - console.log("Form values injected successfully."); - - onSuccess(); + if (isGoogleSheet) { + // Google Sheets flow + const intentResult = await classifyIntent(userInput); + if (intentResult?.intent === "unknown") { + throw new Error("Unable to determine automation intent"); + } + + const sheetMetadata = await extractMetadata(); + if (!sheetMetadata) { + throw new Error("Failed to extract sheet metadata"); + } + + await executeAutomation(intentResult?.intent || "unknown", userInput); + onSuccess(); + } else { + // Form automation flow + console.log("Starting form automation..."); + const htmlContent = await extractHtml(); + const structuredForm = await structureForm(htmlContent); + const aiData = await sendToAI(userInput, structuredForm); + + onClearNotifications(); + onAddNotification( + AI_RESPONSE_RECEIVED_NOTIFICATION.description, + "replace" + ); + onAddNotification( + INJECTING_FORM_VALUES_NOTIFICATION.description, + "add" + ); + + await injectFields(aiData); + onSuccess(); + } } catch (error: any) { console.error("Automation error:", error); const errorMessage = error.message || "An unexpected error occurred during automation."; - onError(errorMessage); // Call error handler from Home.tsx + onError(errorMessage); } finally { setIsRunning(false); } diff --git a/client/src/hooks/useSheetAutomation.ts b/client/src/hooks/useSheetAutomation.ts new file mode 100644 index 0000000..bb2d48f --- /dev/null +++ b/client/src/hooks/useSheetAutomation.ts @@ -0,0 +1,93 @@ +import { useState } from "react"; +import { useSheetMetadata } from "./useSheetMetadata"; +import { sheetsService } from "../lib/services/sheets"; +import { IntentClassification } from "../types/sheets"; +import api from "@/lib/api"; + +export const useSheetAutomation = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { metadata, extractMetadata } = useSheetMetadata(); + + const classifyIntent = async ( + prompt: string + ): Promise => { + setLoading(true); + try { + const response = await api.post("/sheets/classify", { prompt }); + return response.data; + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to classify intent" + ); + return null; + } finally { + setLoading(false); + } + }; + + const executeAutomation = async ( + intent: string, + prompt: string + ): Promise => { + setLoading(true); + try { + const sheetMetadata = await extractMetadata(); + if (!sheetMetadata) throw new Error("Failed to extract sheet metadata"); + + const response = await api.post("/sheets/automate", { + intent, + prompt, + sheetMetadata, + }); + + const result = response.data; + + // Execute the appropriate action based on intent + switch (intent) { + case "data_entry": + await sheetsService.insertData(sheetMetadata, result.data); + break; + case "chart_generation": + await sheetsService.insertChart(sheetMetadata, result.chartConfig); + break; + case "sheet_modification": { + const requests = result.modifications.map((mod: any) => ({ + updateCells: { + range: { + sheetId: sheetMetadata.sheetId, + startRowIndex: mod.range.startRow - 1, + endRowIndex: mod.range.endRow, + startColumnIndex: mod.range.startColumn.charCodeAt(0) - 65, + endColumnIndex: mod.range.endColumn.charCodeAt(0) - 64, + }, + rows: mod.values.map((row: any[]) => ({ + values: row.map((cell: any) => ({ + userEnteredValue: { stringValue: cell.toString() }, + })), + })), + fields: "userEnteredValue", + }, + })); + + await (window as any).gapi.client.sheets.spreadsheets.batchUpdate({ + spreadsheetId: sheetMetadata.spreadsheetId, + resource: { requests }, + }); + break; + } + } + + return true; + } catch (err) { + setError( + err instanceof Error ? err.message : "Failed to execute automation" + ); + return false; + } finally { + setLoading(false); + } + }; + + return { classifyIntent, executeAutomation, loading, error, metadata }; +}; diff --git a/client/src/hooks/useSheetDetector.ts b/client/src/hooks/useSheetDetector.ts new file mode 100644 index 0000000..7c53113 --- /dev/null +++ b/client/src/hooks/useSheetDetector.ts @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; + +export const useSheetDetector = () => { + const [isGoogleSheet, setIsGoogleSheet] = useState(false); + + useEffect(() => { + const checkIfGoogleSheet = () => { + if (chrome && chrome.tabs) { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + const currentTab = tabs[0]; + const title = currentTab?.title || ""; + setIsGoogleSheet(title.includes("Google Sheets")); + }); + } + }; + + checkIfGoogleSheet(); + + // Listen for tab updates + const handleTabUpdate = ( + _tabId: number, + changeInfo: chrome.tabs.TabChangeInfo, + tab: chrome.tabs.Tab + ) => { + if (tab.active && changeInfo.title) { + setIsGoogleSheet(changeInfo.title.includes("Google Sheets")); + } + }; + + if (chrome && chrome.tabs && chrome.tabs.onUpdated) { + chrome.tabs.onUpdated.addListener(handleTabUpdate); + } + + // Cleanup listener + return () => { + if (chrome && chrome.tabs && chrome.tabs.onUpdated) { + chrome.tabs.onUpdated.removeListener(handleTabUpdate); + } + }; + }, []); + + return { isGoogleSheet }; +}; diff --git a/client/src/hooks/useSheetMetadata.ts b/client/src/hooks/useSheetMetadata.ts new file mode 100644 index 0000000..335a693 --- /dev/null +++ b/client/src/hooks/useSheetMetadata.ts @@ -0,0 +1,195 @@ +import { useState, useEffect } from "react"; +import { SheetMetadata, SheetRange } from "../types/sheets"; + +export const useSheetMetadata = () => { + const [metadata, setMetadata] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [isApiReady, setIsApiReady] = useState(false); + + useEffect(() => { + const initializeGoogleSheetsAPI = async () => { + try { + console.log("Initializing Google Sheets API..."); + await new Promise((resolve, reject) => { + if ((window as any).gapi?.client?.sheets) { + console.log("Google Sheets API already loaded"); + resolve(); + return; + } + + (window as any).gapi.load("client", async () => { + try { + await (window as any).gapi.client.init({ + apiKey: process.env.VITE_GOOGLE_API_KEY, + discoveryDocs: [ + "https://sheets.googleapis.com/$discovery/rest?version=v4", + ], + clientId: process.env.VITE_GOOGLE_CLIENT_ID, + scope: "https://www.googleapis.com/auth/spreadsheets", + }); + + console.log("Google Sheets API initialized successfully"); + resolve(); + } catch (error) { + console.error("Failed to initialize Google Sheets API:", error); + reject(error); + } + }); + }); + setIsApiReady(true); + } catch (error) { + console.error("Error initializing Google Sheets API:", error); + setError("Failed to initialize Google Sheets API"); + } + }; + + initializeGoogleSheetsAPI(); + }, []); + + const extractMetadata = async (): Promise => { + if (!isApiReady) { + console.error("Google Sheets API not ready"); + throw new Error("Google Sheets API not initialized"); + } + + setLoading(true); + try { + console.log("Starting metadata extraction..."); + + // Check if GAPI is loaded + if (!(window as any).gapi?.client?.sheets) { + console.error("Google Sheets API not loaded"); + throw new Error("Google Sheets API not initialized"); + } + + const spreadsheetId = getSpreadsheetIdFromUrl(); + console.log("Extracted spreadsheet ID:", spreadsheetId); + + // Use Google Sheets API to get metadata + console.log("Fetching spreadsheet data..."); + const response = await ( + window as any + ).gapi.client.sheets.spreadsheets.get({ + spreadsheetId, + ranges: [], + includeGridData: true, + }); + console.log("Spreadsheet response:", response); + + const activeSheet = response.result.sheets[0]; + console.log("Active sheet:", activeSheet); + + const gridProperties = activeSheet.properties.gridProperties; + console.log("Grid properties:", gridProperties); + + console.log("Getting active range..."); + const activeRange = getActiveRange(); + console.log("Active range:", activeRange); + + console.log("Extracting headers..."); + const headers = await extractHeaders(); + console.log("Extracted headers:", headers); + + const metadata: SheetMetadata = { + spreadsheetId: response.result.spreadsheetId, + sheetId: activeSheet.properties.sheetId.toString(), + sheetTitle: activeSheet.properties.title, + activeRange, + headers, + totalRows: gridProperties.rowCount, + totalColumns: gridProperties.columnCount, + }; + + console.log("Final metadata:", metadata); + setMetadata(metadata); + return metadata; + } catch (err) { + console.error("Metadata extraction error:", err); + console.error("Error stack:", (err as Error).stack); + const errorMessage = + err instanceof Error ? err.message : "Failed to extract sheet metadata"; + setError(errorMessage); + return null; + } finally { + setLoading(false); + } + }; + + const getSpreadsheetIdFromUrl = (): string => { + const url = window.location.href; + console.log("Current URL:", url); + const matches = url.match(/spreadsheets\/d\/([a-zA-Z0-9-_]+)/); + if (!matches) { + console.error("Failed to extract spreadsheet ID from URL"); + throw new Error("Invalid Google Sheets URL"); + } + return matches[1]; + }; + + const getActiveRange = (): SheetRange => { + try { + const selection = ( + window as any + ).gapi.client.sheets.spreadsheets.getSelection(); + if (!selection) { + return { + startRow: 1, + endRow: 10, // Default to first 10 rows if no selection + startColumn: "A", + endColumn: "Z", // Default to first 26 columns if no selection + }; + } + return { + startRow: selection.startRowIndex + 1, + endRow: selection.endRowIndex + 1, + startColumn: columnIndexToLetter(selection.startColumnIndex), + endColumn: columnIndexToLetter(selection.endColumnIndex), + }; + } catch (error) { + console.error("Failed to get active range:", error); + return { + startRow: 1, + endRow: 10, + startColumn: "A", + endColumn: "Z", + }; + } + }; + + const extractHeaders = async (): Promise => { + try { + const spreadsheetId = getSpreadsheetIdFromUrl(); + const range = "A1:Z1"; // First row, columns A-Z + + const response = await ( + window as any + ).gapi.client.sheets.spreadsheets.values.get({ + spreadsheetId, + range, + }); + + const headerRow = response.result.values[0]; + if (!headerRow || !headerRow.length) { + throw new Error("No headers found in the first row"); + } + + return headerRow.map((header: string) => header.trim()); + } catch (error) { + console.error("Failed to extract headers:", error); + throw new Error("Failed to extract sheet headers"); + } + }; + + // Helper function to convert column index to letter + const columnIndexToLetter = (index: number): string => { + let letter = ""; + while (index >= 0) { + letter = String.fromCharCode(65 + (index % 26)) + letter; + index = Math.floor(index / 26) - 1; + } + return letter; + }; + + return { metadata, loading, error, extractMetadata, isApiReady }; +}; diff --git a/client/src/lib/services/sheets.ts b/client/src/lib/services/sheets.ts new file mode 100644 index 0000000..9848d13 --- /dev/null +++ b/client/src/lib/services/sheets.ts @@ -0,0 +1,84 @@ +import { ChartConfig, SheetMetadata, SheetRange } from "../../types/sheets"; + +export class GoogleSheetsService { + private async initializeApi(): Promise { + await new Promise((resolve, reject) => { + (window as any).gapi.load("client", async () => { + try { + await (window as any).gapi.client.init({ + apiKey: process.env.VITE_GOOGLE_API_KEY, + discoveryDocs: [ + "https://sheets.googleapis.com/$discovery/rest?version=v4", + ], + scope: "https://www.googleapis.com/auth/spreadsheets", + }); + resolve(); + } catch (err) { + reject(err); + } + }); + }); + } + + async insertData(metadata: SheetMetadata, data: any[][]): Promise { + await this.initializeApi(); + const request = { + spreadsheetId: metadata.spreadsheetId, + range: `${metadata.sheetTitle}!A1`, // Adjust based on actual range + valueInputOption: "USER_ENTERED", + resource: { + values: data, + }, + }; + + await (window as any).gapi.client.sheets.spreadsheets.values.update( + request + ); + } + + async insertChart( + metadata: SheetMetadata, + chartConfig: ChartConfig + ): Promise { + await this.initializeApi(); + const request = { + spreadsheetId: metadata.spreadsheetId, + resource: { + requests: [ + { + addChart: { + chart: { + spec: { + title: chartConfig.title, + basicChart: { + chartType: chartConfig.type.toUpperCase(), + domains: [{ range: this.convertRange(chartConfig.range) }], + series: [{ range: this.convertRange(chartConfig.range) }], + headerCount: 1, + }, + }, + position: { + overlayPosition: { + anchorCell: { + sheetId: metadata.sheetId, + rowIndex: 0, + columnIndex: 0, + }, + }, + }, + }, + }, + }, + ], + }, + }; + + await (window as any).gapi.client.sheets.spreadsheets.batchUpdate(request); + } + + private convertRange(range: SheetRange): string { + return `${range.startColumn}${range.startRow}:${range.endColumn}${range.endRow}`; + } +} + +export const sheetsService = new GoogleSheetsService(); diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index f815c6f..477bfcb 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -196,7 +196,6 @@ const Home: React.FC = () => { name: "INPUT_ERROR", description: "Please provide instructions for the automation.", icon: "⚠️", - color: "#F59E0B", // Amber }); return; } diff --git a/client/src/types/sheets.ts b/client/src/types/sheets.ts new file mode 100644 index 0000000..6e0b8d5 --- /dev/null +++ b/client/src/types/sheets.ts @@ -0,0 +1,31 @@ +export interface SheetRange { + startRow: number; + endRow: number; + startColumn: string; + endColumn: string; +} + +export interface SheetMetadata { + spreadsheetId: string; + sheetId: string; + sheetTitle: string; + activeRange: SheetRange; + headers: string[]; + totalRows: number; + totalColumns: number; +} + +export interface ChartConfig { + type: 'bar' | 'line' | 'pie' | 'scatter'; + title: string; + range: SheetRange; + options: Record; +} + +export type SheetIntent = 'data_entry' | 'chart_generation' | 'sheet_modification' | 'unknown'; + +export interface IntentClassification { + intent: SheetIntent; + confidence: number; + justification: string; +} \ No newline at end of file diff --git a/server/app.js b/server/app.js index febe0ba..6c7cde6 100644 --- a/server/app.js +++ b/server/app.js @@ -11,6 +11,7 @@ const health = require("./routes/health"); const authRoutes = require("./routes/auth"); const userRoutes = require("./routes/user"); const requestRoutes = require("./routes/requests"); +const sheetsRoutes = require("./routes/sheets"); // Add this line const app = express(); @@ -32,6 +33,7 @@ app.use("/api/health", health); app.use("/api/auth", authRoutes); app.use("/api/user", userRoutes); app.use("/api/requests", requestRoutes); +app.use("/api/sheets", sheetsRoutes); // Add this line app.listen(port, () => { console.log("Server running on port", port); diff --git a/server/routes/sheets.js b/server/routes/sheets.js new file mode 100644 index 0000000..9e8272c --- /dev/null +++ b/server/routes/sheets.js @@ -0,0 +1,32 @@ +const express = require('express'); +const router = express.Router(); +const authenticateJWT = require('../middleware/auth'); +const checkCredits = require('../middleware/checkCredits'); +const sheetClassifier = require('../services/sheetClassifier'); +const sheetAutomation = require('../services/sheetAutomation'); + +router.post('/classify', authenticateJWT, checkCredits, async (req, res) => { + try { + const { prompt } = req.body; + const classification = await sheetClassifier.classifyIntent(prompt); + res.json(classification); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.post('/automate', authenticateJWT, checkCredits, async (req, res) => { + try { + const { intent, prompt, sheetMetadata } = req.body; + const result = await sheetAutomation.processAutomation( + intent, + prompt, + sheetMetadata + ); + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/server/services/openai.js b/server/services/openai.js index 2a98829..e019d25 100644 --- a/server/services/openai.js +++ b/server/services/openai.js @@ -17,14 +17,6 @@ class OpenAIService { // New method for mapping with the detailed pre-prompt async mapUserInputToForm(userInput, pageStructure) { - console.log( - "mapUserInputToForm: UserInput:", - JSON.stringify(userInput, null, 2) - ); - console.log( - "mapUserInputToForm: PageStructure:", - JSON.stringify(pageStructure, null, 2) - ); const prePromptTemplate = ` You are an intelligent agent that maps freeform user input to a structured web form for automated filling. @@ -218,6 +210,62 @@ class OpenAIService { // constructPrompt and parseResponse for the older PageStructure format can remain if still needed // constructPrompt(userInput, formStructure) { ... } // parseResponse(apiResponse, originalPageStructure) { ... } + + // Add this method to the OpenAIService class + async classifyIntent(prompt) { + try { + const response = await this.client.post("/chat/completions", { + model: "o4-mini", + messages: [ + { + role: "system", + content: + "You are a classifier that analyzes Google Sheets automation intents. Respond only with a JSON object containing 'intent', 'confidence', and 'justification' fields.", + }, + { + role: "user", + content: prompt, + }, + ], + temperature: 1, //Only the default (1) value is supported + }); + + if (!response.data.choices || !response.data.choices[0].message) { + throw new Error("Invalid response structure from OpenAI"); + } + + const content = response.data.choices[0].message.content; + let result; + try { + result = JSON.parse(content.trim()); + if (!result.intent || !result.confidence || !result.justification) { + throw new Error("Missing required fields in classification response"); + } + if ( + ![ + "data_entry", + "chart_generation", + "sheet_modification", + "unknown", + ].includes(result.intent) + ) { + throw new Error("Invalid intent classification"); + } + } catch (parseError) { + throw new Error( + `Failed to parse classification response: ${parseError.message}` + ); + } + + return result; + } catch (error) { + console.error( + "OpenAI API Error (classifyIntent):", + error.response ? error.response.data : error.message + ); + throw new Error(`Failed to classify intent: ${error.message}`); + } + } } module.exports = new OpenAIService(); diff --git a/server/services/sheetAutomation.js b/server/services/sheetAutomation.js new file mode 100644 index 0000000..c740e3b --- /dev/null +++ b/server/services/sheetAutomation.js @@ -0,0 +1,32 @@ +class SheetAutomationService { + constructor() { + this.openaiService = require('./openai'); + } + + async processAutomation(intent, userPrompt, sheetMetadata) { + switch(intent) { + case 'data_entry': + return this.handleDataEntry(userPrompt, sheetMetadata); + case 'chart_generation': + return this.handleChartGeneration(userPrompt, sheetMetadata); + case 'sheet_modification': + return this.handleSheetModification(userPrompt, sheetMetadata); + default: + throw new Error('Unsupported intent'); + } + } + + async handleDataEntry(prompt, metadata) { + // Implementation for data entry automation + } + + async handleChartGeneration(prompt, metadata) { + // Implementation for chart generation + } + + async handleSheetModification(prompt, metadata) { + // Implementation for sheet modifications + } +} + +module.exports = new SheetAutomationService(); \ No newline at end of file diff --git a/server/services/sheetClassifier.js b/server/services/sheetClassifier.js new file mode 100644 index 0000000..6808fe3 --- /dev/null +++ b/server/services/sheetClassifier.js @@ -0,0 +1,23 @@ +class SheetClassifierService { + constructor() { + this.openaiService = require('./openai'); + } + + async classifyIntent(userPrompt) { + const prompt = `Classify the following Google Sheets automation intent: +${userPrompt} + +Possible intents: +- data_entry +- chart_generation +- sheet_modification +- unknown + +Respond in JSON format with intent, confidence, and justification.`; + + const response = await this.openaiService.classifyIntent(prompt); + return response; + } +} + +module.exports = new SheetClassifierService(); \ No newline at end of file From ef117a0ea071deef860d5060838a339f41116b09 Mon Sep 17 00:00:00 2001 From: Aiman Kandakji Date: Sat, 21 Jun 2025 22:52:23 +0300 Subject: [PATCH 2/2] feat(auth): integrate google oauth for sheet automation Add new useGoogleAuth hook to handle Google OAuth authentication flow. Update manifest.json with new client ID and required scopes. Modify useAutomationRunner to authenticate with Google before executing sheet operations. The changes enable secure authentication with Google Sheets API, allowing for proper authorization when performing spreadsheet automations. The new hook manages token state and provides methods for authentication and token clearing. --- client/public/manifest.json | 9 ++++-- client/src/hooks/useAutomationRunner.ts | 41 +++++++++++++++---------- client/src/hooks/useGoogleAuth.ts | 40 ++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 client/src/hooks/useGoogleAuth.ts diff --git a/client/public/manifest.json b/client/public/manifest.json index 012cdd4..e9bc105 100644 --- a/client/public/manifest.json +++ b/client/public/manifest.json @@ -25,9 +25,14 @@ "tabs" ], "oauth2": { - "client_id": "581875153522-44qhjt4693geitf1cis0bf91rkf7dmja.apps.googleusercontent.com", - "scopes": ["profile", "email"] + "client_id": "581875153522-i2is24sdjrq7ekvohqfqiljj2gthhdrd.apps.googleusercontent.com", + "scopes": [ + "https://www.googleapis.com/auth/spreadsheets", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/drive.file" + ] }, + "host_permissions": ["http://*/*", "https://*/*"], "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self';" diff --git a/client/src/hooks/useAutomationRunner.ts b/client/src/hooks/useAutomationRunner.ts index eff3528..b155e33 100644 --- a/client/src/hooks/useAutomationRunner.ts +++ b/client/src/hooks/useAutomationRunner.ts @@ -8,6 +8,7 @@ import { Item as NotificationItemProps } from "@/components/page/home/Notificati import { useSheetDetector } from "./useSheetDetector"; import { useSheetAutomation } from "./useSheetAutomation"; import { useSheetMetadata } from "./useSheetMetadata"; +import { useGoogleAuth } from "./useGoogleAuth"; // Define the props for notification handlers interface AutomationRunnerProps { @@ -58,6 +59,7 @@ export const useAutomationRunner = (props: AutomationRunnerProps) => { const { isGoogleSheet } = useSheetDetector(); const { classifyIntent, executeAutomation } = useSheetAutomation(); const { extractMetadata } = useSheetMetadata(); + const { authenticateWithGoogle } = useGoogleAuth(); const runAutomation = async (userInput: string) => { if (!auth || !auth.token) { @@ -69,21 +71,31 @@ export const useAutomationRunner = (props: AutomationRunnerProps) => { setIsRunning(true); try { if (isGoogleSheet) { - // Google Sheets flow - const intentResult = await classifyIntent(userInput); - if (intentResult?.intent === "unknown") { - throw new Error("Unable to determine automation intent"); - } + // Google Sheets authentication + try { + const googleToken = await authenticateWithGoogle(); + console.log("Successfully authenticated with Google", googleToken); - const sheetMetadata = await extractMetadata(); - if (!sheetMetadata) { - throw new Error("Failed to extract sheet metadata"); - } + // Google Sheets flow + const intentResult = await classifyIntent(userInput); + if (intentResult?.intent === "unknown") { + throw new Error("Unable to determine automation intent"); + } - await executeAutomation(intentResult?.intent || "unknown", userInput); - onSuccess(); + const sheetMetadata = await extractMetadata(); + if (!sheetMetadata) { + throw new Error("Failed to extract sheet metadata"); + } + + await executeAutomation(intentResult?.intent || "unknown", userInput); + onSuccess(); + } catch (googleError) { + console.error("Google authentication error:", googleError); + onError("Failed to authenticate with Google Sheets"); + return; + } } else { - // Form automation flow + // Existing form automation flow console.log("Starting form automation..."); const htmlContent = await extractHtml(); const structuredForm = await structureForm(htmlContent); @@ -112,8 +124,5 @@ export const useAutomationRunner = (props: AutomationRunnerProps) => { } }; - return { - runAutomation, - isRunning, - }; + return { runAutomation, isRunning }; }; diff --git a/client/src/hooks/useGoogleAuth.ts b/client/src/hooks/useGoogleAuth.ts new file mode 100644 index 0000000..c8e063c --- /dev/null +++ b/client/src/hooks/useGoogleAuth.ts @@ -0,0 +1,40 @@ +import { useCallback, useState } from 'react'; + +export const useGoogleAuth = () => { + const [token, setToken] = useState(null); + const [error, setError] = useState(null); + + const authenticateWithGoogle = useCallback((): Promise => { + return new Promise((resolve, reject) => { + chrome.identity.getAuthToken({ interactive: true }, (token) => { + if (chrome.runtime.lastError || !token) { + const error = chrome.runtime.lastError?.message || 'Authentication failed'; + console.error('OAuth error:', error); + setError(error); + reject(error); + } else { + console.log('Google OAuth token received'); + setToken(token); + resolve(token); + } + }); + }); + }, []); + + const clearToken = useCallback(() => { + if (token) { + chrome.identity.removeCachedAuthToken({ token }, () => { + setToken(null); + setError(null); + }); + } + }, [token]); + + return { + token, + error, + authenticateWithGoogle, + clearToken, + isAuthenticated: !!token + }; +};