Skip to content

Commit 977b5ef

Browse files
committed
add OpenAI support
Signed-off-by: Bohaska <73286691+Bohaska@users.noreply.github.com>
1 parent ef53b7b commit 977b5ef

File tree

11 files changed

+953
-483
lines changed

11 files changed

+953
-483
lines changed

chrome-extension/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@extension/shared": "workspace:*",
2424
"@extension/storage": "workspace:*",
2525
"@google/genai": "^1.4.0",
26+
"openai": "^5.8.1",
2627
"webextension-polyfill": "^0.12.0"
2728
},
2829
"devDependencies": {

chrome-extension/src/background/index.ts

Lines changed: 188 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1-
import 'webextension-polyfill';
2-
import { GoogleGenAI, Type } from '@google/genai';
1+
import "webextension-polyfill";
2+
import { GoogleGenAI, Type } from "@google/genai";
3+
import OpenAI from "openai";
34

45
// Temporary storage for autofill requests, as service workers are stateless
5-
const autofillRequests: Record<number, { profile: any; apiKey: string; selectedAiModel: string }> = {}; // Change type to 'any' for structured profile
6+
const autofillRequests: Record<
7+
number,
8+
{
9+
profile: any;
10+
geminiApiKey: string;
11+
selectedGeminiModel: string;
12+
openAiApiKey: string;
13+
selectedOpenAiModel: string;
14+
selectedProvider: "gemini" | "openai";
15+
}
16+
> = {}; // Change type to 'any' for structured profile
617

718
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
819
// Mark sendResponse as asynchronous to allow handlers to use it later
@@ -27,10 +38,19 @@ async function handleAutofillRequest(payload: { tabId: number; profile: any; api
2738
return;
2839
}
2940

30-
const { selectedAiModel } = await chrome.storage.local.get('selectedAiModel');
41+
const { model: selectedGeminiModel } = await chrome.storage.local.get('ai-model-storage-key');
42+
const { apiKey: openAiApiKey, model: selectedOpenAiModel } = await chrome.storage.local.get('openai-storage-key');
43+
const { provider: selectedProvider } = await chrome.storage.local.get('provider-storage-key');
3144

3245
// Store the payload temporarily
33-
autofillRequests[tabId] = { profile, apiKey, selectedAiModel: selectedAiModel || 'gemini-2.5-flash-lite-preview-06-17' };
46+
autofillRequests[tabId] = {
47+
profile,
48+
geminiApiKey: apiKey,
49+
selectedGeminiModel: selectedGeminiModel || "gemini-2.5-flash-lite-preview-06-17",
50+
openAiApiKey: openAiApiKey || "",
51+
selectedOpenAiModel: selectedOpenAiModel || "gpt-4.1-mini",
52+
selectedProvider: selectedProvider || "gemini"
53+
};
3454

3555
try {
3656
// Send message to content script to extract form data
@@ -54,23 +74,59 @@ async function handleFormDataExtracted(pageContextItems: any[], tabId: number |
5474
return;
5575
}
5676

57-
const { profile, apiKey, selectedAiModel } = autofillRequests[tabId];
77+
const {
78+
profile,
79+
geminiApiKey,
80+
selectedGeminiModel,
81+
openAiApiKey,
82+
selectedOpenAiModel,
83+
selectedProvider
84+
} = autofillRequests[tabId];
85+
86+
let aiModel: any;
87+
let currentApiKey: string;
88+
let modelName: string;
5889

59-
if (!apiKey) {
60-
console.error('Gemini API Key is missing.');
61-
chrome.runtime.sendMessage({ type: 'UPDATE_POPUP_STATUS', payload: 'Error: Gemini API Key is missing. Please set it in the popup.' });
90+
if (selectedProvider === "gemini") {
91+
if (!geminiApiKey) {
92+
console.error("Gemini API Key is missing.");
93+
chrome.runtime.sendMessage({
94+
type: "UPDATE_POPUP_STATUS",
95+
payload: "Error: Gemini API Key is missing. Please set it in the popup."
96+
});
97+
delete autofillRequests[tabId];
98+
sendResponse({ success: false, error: "Gemini API Key is missing." });
99+
return;
100+
}
101+
aiModel = new GoogleGenAI({ apiKey: geminiApiKey });
102+
modelName = selectedGeminiModel;
103+
currentApiKey = geminiApiKey;
104+
} else if (selectedProvider === "openai") {
105+
if (!openAiApiKey) {
106+
console.error("OpenAI API Key is missing.");
107+
chrome.runtime.sendMessage({
108+
type: "UPDATE_POPUP_STATUS",
109+
payload: "Error: OpenAI API Key is missing. Please set it in the popup."
110+
});
111+
delete autofillRequests[tabId];
112+
sendResponse({ success: false, error: "OpenAI API Key is missing." });
113+
return;
114+
}
115+
aiModel = new OpenAI({ apiKey: openAiApiKey });
116+
modelName = selectedOpenAiModel;
117+
currentApiKey = openAiApiKey;
118+
} else {
119+
console.error("No AI provider selected.");
120+
chrome.runtime.sendMessage({ type: "UPDATE_POPUP_STATUS", payload: "Error: No AI provider selected." });
62121
delete autofillRequests[tabId];
63-
sendResponse({ success: false, error: 'Gemini API Key is missing.' }); // Send error response
122+
sendResponse({ success: false, error: "No AI provider selected." });
64123
return;
65124
}
66125

67126
// Acknowledge receipt to the content script immediately.
68-
// The actual result of the Gemini call will be communicated via chrome.tabs.sendMessage later.
69-
sendResponse({ success: true, message: 'Form data received, processing with Gemini...' });
70-
chrome.runtime.sendMessage({ type: 'UPDATE_POPUP_STATUS', payload: 'Sending data to Gemini...' });
71-
72-
const ai = new GoogleGenAI({ apiKey: apiKey });
73-
const model = ai.models;
127+
// The actual result of the AI call will be communicated via chrome.tabs.sendMessage later.
128+
sendResponse({ success: true, message: `Form data received, processing with ${selectedProvider}...` });
129+
chrome.runtime.sendMessage({ type: "UPDATE_POPUP_STATUS", payload: `Sending data to ${selectedProvider}...` });
74130

75131
// Sort page context items by their DOM order
76132
pageContextItems.sort((a, b) => a.domOrder - b.domOrder);
@@ -104,75 +160,135 @@ async function handleFormDataExtracted(pageContextItems: any[], tabId: number |
104160
const prompt = `You are an AI assistant specialized in intelligently filling web forms.\nHere is a description of the web page's structure, including text content and form elements, ordered by their appearance in the DOM:\n${pageStructureDescription}\n\nHere is a more structured list of the form elements found on the page, including their unique selectors for interaction:\n${JSON.stringify(formElementsForPrompt, null, 2)}\n\nHere is the user's personal information. Use these details to fill the form:\n${profile}\n\nYour goal is to fill out this form accurately using the provided user information.\nYou have the following tools available:\nfunction fill_text_input(selector: string, value: string, field_type: string)\nfunction select_dropdown_option(selector: string, value: string)\nfunction check_radio_or_checkbox(selector: string, checked: boolean)\n\nBased on the form elements, the surrounding text context, and user data, suggest the next action(s) to take using the available tools. Output your action(s) as a JSON array of tool calls.\n`;
105161

106162
try {
107-
const result = await model.generateContent({
108-
model: selectedAiModel,
109-
contents: [{ role: 'user', parts: [{ text: prompt }] }],
110-
config: {
111-
tools: [
112-
{
113-
functionDeclarations: [
114-
{
115-
name: 'fill_text_input',
116-
parameters: {
117-
type: Type.OBJECT,
118-
properties: {
119-
selector: { type: Type.STRING },
120-
value: { type: Type.STRING },
121-
field_type: { type: Type.STRING },
122-
},
123-
required: ['selector', 'value', 'field_type'],
163+
let toolCalls;
164+
if (selectedProvider === "gemini") {
165+
const geminiResult = await aiModel.models.generateContent({
166+
model: modelName,
167+
contents: [{ role: "user", parts: [{ text: prompt }] }],
168+
config: {
169+
tools: [
170+
{
171+
functionDeclarations: [
172+
{
173+
name: "fill_text_input",
174+
parameters: {
175+
type: Type.OBJECT,
176+
properties: {
177+
selector: { type: Type.STRING },
178+
value: { type: Type.STRING },
179+
field_type: { type: Type.STRING }
180+
},
181+
required: ["selector", "value", "field_type"]
182+
}
124183
},
125-
},
126-
{
127-
name: 'select_dropdown_option',
128-
parameters: {
129-
type: Type.OBJECT,
130-
properties: {
131-
selector: { type: Type.STRING },
132-
value: { type: Type.STRING },
133-
},
134-
required: ['selector', 'value'],
184+
{
185+
name: "select_dropdown_option",
186+
parameters: {
187+
type: Type.OBJECT,
188+
properties: {
189+
selector: { type: Type.STRING },
190+
value: { type: Type.STRING }
191+
},
192+
required: ["selector", "value"]
193+
}
135194
},
195+
{
196+
name: "check_radio_or_checkbox",
197+
parameters: {
198+
type: Type.OBJECT,
199+
properties: {
200+
selector: { type: Type.STRING },
201+
checked: { type: Type.BOOLEAN }
202+
},
203+
required: ["selector", "checked"]
204+
}
205+
}
206+
]
207+
}
208+
]
209+
}
210+
});
211+
toolCalls = geminiResult.functionCalls;
212+
console.log(`${selectedProvider} LLM Raw Response Text:`, geminiResult.text);
213+
} else if (selectedProvider === "openai") {
214+
const tools = [
215+
{
216+
type: "function",
217+
function: {
218+
name: "fill_text_input",
219+
parameters: {
220+
type: "object",
221+
properties: {
222+
selector: { type: "string" },
223+
value: { type: "string" },
224+
field_type: { type: "string" }
136225
},
137-
{
138-
name: 'check_radio_or_checkbox',
139-
parameters: {
140-
type: Type.OBJECT,
141-
properties: {
142-
selector: { type: Type.STRING },
143-
checked: { type: Type.BOOLEAN },
144-
},
145-
required: ['selector', 'checked'],
146-
},
226+
required: ["selector", "value", "field_type"]
227+
}
228+
}
229+
},
230+
{
231+
type: "function",
232+
function: {
233+
name: "select_dropdown_option",
234+
parameters: {
235+
type: "object",
236+
properties: {
237+
selector: { type: "string" },
238+
value: { type: "string" }
147239
},
148-
],
149-
},
150-
],
151-
},
152-
});
153-
154-
// Log the LLM response
155-
const responseText = result.text;
156-
console.log('Gemini LLM Raw Response Text:', responseText);
157-
const toolCalls = result.functionCalls;
158-
console.log('Gemini LLM Function Calls:', toolCalls);
240+
required: ["selector", "value"]
241+
}
242+
}
243+
},
244+
{
245+
type: "function",
246+
function: {
247+
name: "check_radio_or_checkbox",
248+
parameters: {
249+
type: "object",
250+
properties: {
251+
selector: { type: "string" },
252+
checked: { type: "boolean" }
253+
},
254+
required: ["selector", "checked"]
255+
}
256+
}
257+
}
258+
];
259+
260+
const chatCompletion = await aiModel.chat.completions.create({
261+
model: modelName,
262+
messages: [{ role: "user", content: prompt }],
263+
tools: tools,
264+
tool_choice: "auto"
265+
});
266+
267+
toolCalls = chatCompletion.choices[0].message.tool_calls;
268+
console.log(`${selectedProvider} LLM Raw Response Text:`, chatCompletion.choices[0].message.content);
269+
}
270+
271+
console.log(`${selectedProvider} LLM Function Calls:`, toolCalls);
159272

160273
if (toolCalls && toolCalls.length > 0) {
161274
chrome.runtime.sendMessage({ type: 'UPDATE_POPUP_STATUS', payload: 'Filling fields...' });
162275
await chrome.tabs.sendMessage(tabId, { type: 'EXECUTE_ACTIONS', payload: toolCalls });
163276
// No sendResponse here for the original message, it was already sent.
164277
// The content script will receive EXECUTE_ACTIONS directly.
165278
} else {
166-
chrome.runtime.sendMessage({ type: 'UPDATE_POPUP_STATUS', payload: 'No fields to fill or Gemini returned no actions.' });
279+
chrome.runtime.sendMessage({
280+
type: "UPDATE_POPUP_STATUS",
281+
payload: `No fields to fill or ${selectedProvider} returned no actions.`
282+
});
167283
// No sendResponse here.
168284
}
169285

170286
} catch (error: any) { // Explicitly type error as 'any' to access properties
171-
console.error('Gemini API call failed:', error);
172-
let userFriendlyMessage = `Error from Gemini: ${error instanceof Error ? error.message : String(error)}`;
287+
console.error(`${selectedProvider} API call failed:`, error);
288+
let userFriendlyMessage = `Error from ${selectedProvider}: ${error instanceof Error ? error.message : String(error)}`;
173289

174-
// Check for 429 Quota Exceeded error
175-
if (error.response && error.response.status === 429) {
290+
// Check for 429 Quota Exceeded error (Gemini specific)
291+
if (selectedProvider === "gemini" && error.response && error.response.status === 429) {
176292
try {
177293
const errorBody = JSON.parse(await error.response.text());
178294
const quotaMetric = errorBody.error?.details?.[0]?.violations?.[0]?.quotaMetric;
@@ -193,6 +309,9 @@ async function handleFormDataExtracted(pageContextItems: any[], tabId: number |
193309
console.warn('Failed to parse Gemini 429 error response:', parseError);
194310
userFriendlyMessage = `Gemini API Quota Exceeded (429). Could not determine retry time.`;
195311
}
312+
} else if (selectedProvider === "openai" && error.response && error.response.status === 429) {
313+
// OpenAI specific 429 handling (if different)
314+
userFriendlyMessage = `OpenAI API Rate Limit Exceeded. Please try again later.`;
196315
}
197316

198317
chrome.runtime.sendMessage({ type: 'UPDATE_POPUP_STATUS', payload: userFriendlyMessage });

packages/storage/lib/impl/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export * from './example-theme-storage.js';
22
export * from './ai-model-storage.js';
3+
export * from './openai-storage.js';
4+
export * from './provider-storage.js';

packages/storage/lib/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,21 @@ export type AiModelState = {
1111
model: string;
1212
};
1313

14+
export type OpenAiModelState = {
15+
apiKey: string;
16+
model: string;
17+
};
18+
19+
export type ProviderState = {
20+
provider: 'gemini' | 'openai';
21+
};
22+
1423
export type AiModelStorageType = BaseStorageType<AiModelState>;
24+
export type OpenAiStorageType = BaseStorageType<OpenAiModelState> & {
25+
setApiKey: (apiKey: string) => Promise<void>;
26+
setModel: (model: string) => Promise<void>;
27+
};
28+
29+
export type ProviderStorageType = BaseStorageType<ProviderState> & {
30+
setProvider: (provider: 'gemini' | 'openai') => Promise<void>;
31+
};

packages/tsconfig/base.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"skipLibCheck": true,
1515
"forceConsistentCasingInFileNames": true,
1616
"resolveJsonModule": true,
17-
"noImplicitReturns": true,
17+
"noImplicitReturns": false,
1818
"jsx": "react-jsx",
1919
"module": "ESNext",
2020
"moduleResolution": "bundler",

packages/ui/lib/components/ToggleButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { cn } from '@/lib/utils';
1+
import { cn } from '../index';
22
import { useStorage } from '@extension/shared';
33
import { exampleThemeStorage } from '@extension/storage';
44
import type { ComponentPropsWithoutRef } from 'react';

packages/ui/lib/components/error-display/ErrorDisplay.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { ErrorHeader } from '@/lib/components/error-display/ErrorHeader';
2-
import { ErrorResetButton } from '@/lib/components/error-display/ErrorResetButton';
3-
import { ErrorStackTraceList } from '@/lib/components/error-display/ErrorStackTraceList';
1+
import { ErrorHeader } from './ErrorHeader';
2+
import { ErrorResetButton } from './ErrorResetButton';
3+
import { ErrorStackTraceList } from './ErrorStackTraceList';
44

55
export const ErrorDisplay = ({ error, resetErrorBoundary }: { error?: Error; resetErrorBoundary?: () => void }) => (
66
<div className="flex items-center justify-center bg-gray-50 px-4 py-6 sm:px-6 lg:px-8">

0 commit comments

Comments
 (0)