diff --git a/client/public/manifest.json b/client/public/manifest.json index 20961e9..897b855 100644 --- a/client/public/manifest.json +++ b/client/public/manifest.json @@ -29,9 +29,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/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..b155e33 100644 --- a/client/src/hooks/useAutomationRunner.ts +++ b/client/src/hooks/useAutomationRunner.ts @@ -1,12 +1,14 @@ -// ... 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"; +import { useGoogleAuth } from "./useGoogleAuth"; // Define the props for notification handlers interface AutomationRunnerProps { @@ -16,7 +18,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,93 +52,77 @@ 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 { authenticateWithGoogle } = useGoogleAuth(); 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 authentication + try { + const googleToken = await authenticateWithGoogle(); + console.log("Successfully authenticated with Google", googleToken); + + // 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(); + } catch (googleError) { + console.error("Google authentication error:", googleError); + onError("Failed to authenticate with Google Sheets"); + return; + } + } else { + // Existing 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); } }; - 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 + }; +}; 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 53929d3..a7936e9 100644 --- a/server/services/openai.js +++ b/server/services/openai.js @@ -40,18 +40,22 @@ Always return valid JSON matching this schema exactly: "checked": false, "status": "filled" }] -}` +}`, }; const userMessage = { role: "user", - content: `Map the following user input to the form fields. ${userInput.toLowerCase().includes("random") ? "Generate appropriate random values based on each field's type and context." : ""} + content: `Map the following user input to the form fields. ${ + userInput.toLowerCase().includes("random") + ? "Generate appropriate random values based on each field's type and context." + : "" + } Form structure: ${JSON.stringify(pageStructure, null, 2)} User input: -${userInput}` +${userInput}`, }; try { @@ -59,7 +63,7 @@ ${userInput}` model: "gpt-3.5-turbo-0125", messages: [systemMessage, userMessage], temperature: 0.7, - response_format: { type: "json_object" } + response_format: { type: "json_object" }, }); if (!response.data?.choices?.[0]?.message?.content) { @@ -74,11 +78,13 @@ ${userInput}` return { mappedForm, - usage: response.data.usage + usage: response.data.usage, }; } catch (error) { console.error("OpenAI API Error:", error); - throw new Error(`Failed to map user input to form with OpenAI. ${error.message}`); + throw new Error( + `Failed to map user input to form with OpenAI. ${error.message}` + ); } } @@ -186,6 +192,62 @@ ${userInput}` // 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