256 lines
7.5 KiB
JavaScript
256 lines
7.5 KiB
JavaScript
// Background script to handle API calls to OpenAI and manage extension state
|
|
|
|
// Configuration (will be stored in sync storage eventually)
|
|
let config = {
|
|
apiKey: "", // Will be set by user in options
|
|
mem0ApiKey: "", // Will be set by user in options
|
|
model: "gpt-4",
|
|
maxTokens: 2000,
|
|
temperature: 0.7,
|
|
enabledSites: ["youtube.com"],
|
|
};
|
|
|
|
// Track if config is loaded
|
|
let isConfigLoaded = false;
|
|
|
|
// Initialize configuration from storage
|
|
chrome.storage.sync.get(
|
|
["apiKey", "mem0ApiKey", "model", "maxTokens", "temperature", "enabledSites"],
|
|
(result) => {
|
|
if (result.apiKey) config.apiKey = result.apiKey;
|
|
if (result.mem0ApiKey) config.mem0ApiKey = result.mem0ApiKey;
|
|
if (result.model) config.model = result.model;
|
|
if (result.maxTokens) config.maxTokens = result.maxTokens;
|
|
if (result.temperature) config.temperature = result.temperature;
|
|
if (result.enabledSites) config.enabledSites = result.enabledSites;
|
|
|
|
isConfigLoaded = true;
|
|
}
|
|
);
|
|
|
|
// Listen for messages from content script or popup
|
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
// Handle different message types
|
|
switch (request.action) {
|
|
case "sendChatRequest":
|
|
sendChatRequest(request.messages, request.model || config.model)
|
|
.then((response) => sendResponse(response))
|
|
.catch((error) => sendResponse({ error: error.message }));
|
|
return true; // Required for async response
|
|
|
|
case "saveConfig":
|
|
saveConfig(request.config)
|
|
.then(() => sendResponse({ success: true }))
|
|
.catch((error) => sendResponse({ error: error.message }));
|
|
return true;
|
|
|
|
case "getConfig":
|
|
// If config isn't loaded yet, load it first
|
|
if (!isConfigLoaded) {
|
|
chrome.storage.sync.get(
|
|
[
|
|
"apiKey",
|
|
"mem0ApiKey",
|
|
"model",
|
|
"maxTokens",
|
|
"temperature",
|
|
"enabledSites",
|
|
],
|
|
(result) => {
|
|
if (result.apiKey) config.apiKey = result.apiKey;
|
|
if (result.mem0ApiKey) config.mem0ApiKey = result.mem0ApiKey;
|
|
if (result.model) config.model = result.model;
|
|
if (result.maxTokens) config.maxTokens = result.maxTokens;
|
|
if (result.temperature) config.temperature = result.temperature;
|
|
if (result.enabledSites) config.enabledSites = result.enabledSites;
|
|
isConfigLoaded = true;
|
|
sendResponse({ config });
|
|
}
|
|
);
|
|
return true;
|
|
}
|
|
sendResponse({ config });
|
|
return false;
|
|
|
|
case "openOptions":
|
|
// Open options page
|
|
chrome.runtime.openOptionsPage(() => {
|
|
if (chrome.runtime.lastError) {
|
|
console.error(
|
|
"Error opening options page:",
|
|
chrome.runtime.lastError
|
|
);
|
|
// Fallback: Try to open directly in a new tab
|
|
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") });
|
|
}
|
|
sendResponse({ success: true });
|
|
});
|
|
return true;
|
|
|
|
case "toggleChat":
|
|
// Forward the toggle request to the active tab
|
|
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
|
if (tabs[0]) {
|
|
chrome.tabs
|
|
.sendMessage(tabs[0].id, { action: "toggleChat" })
|
|
.then((response) => sendResponse(response))
|
|
.catch((error) => sendResponse({ error: error.message }));
|
|
} else {
|
|
sendResponse({ error: "No active tab found" });
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
});
|
|
|
|
// Handle extension icon click - toggle chat visibility
|
|
chrome.action.onClicked.addListener((tab) => {
|
|
chrome.tabs
|
|
.sendMessage(tab.id, { action: "toggleChat" })
|
|
.catch((error) => console.error("Error toggling chat:", error));
|
|
});
|
|
|
|
// Save configuration to sync storage
|
|
async function saveConfig(newConfig) {
|
|
// Validate API key if provided
|
|
if (newConfig.apiKey) {
|
|
try {
|
|
const isValid = await validateApiKey(newConfig.apiKey);
|
|
if (!isValid) {
|
|
throw new Error("Invalid API key");
|
|
}
|
|
} catch (error) {
|
|
throw new Error(`API key validation failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Update local config
|
|
config = { ...config, ...newConfig };
|
|
|
|
// Save to sync storage
|
|
return chrome.storage.sync.set(newConfig);
|
|
}
|
|
|
|
// Validate OpenAI API key with a simple request
|
|
async function validateApiKey(apiKey) {
|
|
try {
|
|
const response = await fetch("https://api.openai.com/v1/models", {
|
|
method: "GET",
|
|
headers: {
|
|
Authorization: `Bearer ${apiKey}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`API returned ${response.status}`);
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error("API key validation error:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Send a chat request to OpenAI API
|
|
async function sendChatRequest(messages, model) {
|
|
// Check if API key is set
|
|
if (!config.apiKey) {
|
|
return {
|
|
error:
|
|
"API key not configured. Please set your OpenAI API key in the extension options.",
|
|
};
|
|
}
|
|
|
|
try {
|
|
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${config.apiKey}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
model: model || config.model,
|
|
messages: messages.map((msg) => ({
|
|
role: msg.role,
|
|
content: msg.content,
|
|
})),
|
|
max_tokens: config.maxTokens,
|
|
temperature: config.temperature,
|
|
stream: true, // Enable streaming
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(
|
|
errorData.error?.message || `API returned ${response.status}`
|
|
);
|
|
}
|
|
|
|
// Create a ReadableStream from the response
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = "";
|
|
|
|
// Process the stream
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
// Decode the chunk and add to buffer
|
|
buffer += decoder.decode(value, { stream: true });
|
|
|
|
// Process complete lines
|
|
const lines = buffer.split("\n");
|
|
buffer = lines.pop() || ""; // Keep the last incomplete line in the buffer
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith("data: ")) {
|
|
const data = line.slice(6);
|
|
if (data === "[DONE]") {
|
|
// Stream complete
|
|
return { done: true };
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(data);
|
|
if (parsed.choices[0].delta.content) {
|
|
// Send the chunk to the content script
|
|
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
|
if (tabs[0]) {
|
|
chrome.tabs.sendMessage(tabs[0].id, {
|
|
action: "streamChunk",
|
|
chunk: parsed.choices[0].delta.content,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error("Error parsing chunk:", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { done: true };
|
|
} catch (error) {
|
|
console.error("Error sending chat request:", error);
|
|
return { error: error.message };
|
|
}
|
|
}
|
|
|
|
// Future: Add mem0 integration functions here
|
|
// When ready, replace with actual implementation
|
|
function mem0Integration() {
|
|
// Placeholder for future mem0 integration
|
|
return {
|
|
getUserMemories: async (userId) => {
|
|
return { memories: [] };
|
|
},
|
|
saveMemory: async (userId, memory) => {
|
|
return { success: true };
|
|
},
|
|
};
|
|
}
|