Skip to content

Commit fff1a6b

Browse files
committed
replace gpt with openrouter, add api key in settings
1 parent b0c541c commit fff1a6b

File tree

8 files changed

+230
-86
lines changed

8 files changed

+230
-86
lines changed

manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
],
1919
"host_permissions": [
2020
"https://chat.openai.com/api/auth/session",
21-
"https://chatgpt.com/api/auth/session"
21+
"https://chatgpt.com/api/auth/session",
22+
"https://openrouter.ai/*"
2223
],
2324
"background": {
2425
"service_worker": "dist/background/background.js",
@@ -27,7 +28,6 @@
2728
"content_scripts": [
2829
{
2930
"js": [
30-
"dist/content-script/get-gpt-access-token.js",
3131
"dist/content-script/get-user-code.js",
3232
"dist/content-script/update-solutions-tab.js",
3333
"dist/content-script/update-description-tab.js",

src/background/background.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ chrome.runtime.onInstalled.addListener(() => {
2424
console.error(error);
2525
});
2626

27+
// Initialize OpenRouter API key as empty if not set
28+
chrome.storage.local.get(['openRouterApiKey'], (result) => {
29+
if (!result.openRouterApiKey) {
30+
chrome.storage.local.set({ openRouterApiKey: '' });
31+
}
32+
});
33+
2734
// Load default settings
2835
chrome.storage.local.set({ fontSize: 14 });
2936
chrome.storage.local.set({ showExamples: true });

src/background/chatgpt/fetch-sse.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,44 @@ import { streamAsyncIterable } from './stream-async-iterable.js';
88

99
export async function fetchSSE(
1010
resource: string,
11-
options: RequestInit & { onMessage: (message: string) => void }) {
11+
options: {
12+
onMessage: (message: string) => void;
13+
method?: string;
14+
headers?: Record<string, string>;
15+
body?: string;
16+
}
17+
): Promise<void> {
1218
const { onMessage, ...fetchOptions } = options;
1319
const resp = await fetch(resource, fetchOptions);
1420
if (!resp.ok) {
15-
const error = await resp.json().catch(() => ({}));
16-
throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`);
21+
throw new Error(`HTTP error! status: ${resp.status}`);
22+
}
23+
const reader = resp.body?.getReader();
24+
if (!reader) {
25+
throw new Error('Response body is not readable');
1726
}
18-
const parser = createParser((event) => {
19-
if (event.type === 'event') {
20-
onMessage(event.data);
27+
28+
const decoder = new TextDecoder();
29+
let buffer = '';
30+
31+
while (true) {
32+
const { done, value } = await reader.read();
33+
if (done) break;
34+
35+
buffer += decoder.decode(value, { stream: true });
36+
const lines = buffer.split('\n');
37+
buffer = lines.pop() || '';
38+
39+
for (const line of lines) {
40+
const trimmedLine = line.trim();
41+
if (trimmedLine === '') continue;
42+
if (trimmedLine === 'data: [DONE]') {
43+
onMessage('[DONE]');
44+
return;
45+
}
46+
if (trimmedLine.startsWith('data: ')) {
47+
onMessage(trimmedLine.slice(6));
48+
}
2149
}
22-
});
23-
for await (const chunk of streamAsyncIterable(resp.body || new ReadableStream())) {
24-
const str = new TextDecoder().decode(chunk);
25-
parser.feed(str);
2650
}
2751
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export interface AIProvider {
2+
generateAnswer(params: {
3+
prompt: string,
4+
onEvent: (arg: { type: string, data?: { text: string } }) => void
5+
}): Promise<void>;
6+
}
7+
8+
export class OpenRouterProvider implements AIProvider {
9+
private readonly apiKey: string;
10+
private readonly model: string;
11+
12+
constructor(apiKey: string, model: string = 'openai/gpt-3.5-turbo') {
13+
this.apiKey = apiKey;
14+
this.model = model;
15+
}
16+
17+
async generateAnswer(params: { prompt: string, onEvent: (arg: { type: string, data?: { text: string } }) => void }) {
18+
try {
19+
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
20+
method: 'POST',
21+
headers: {
22+
'Content-Type': 'application/json',
23+
'Authorization': `Bearer ${this.apiKey}`,
24+
'HTTP-Referer': 'https://github.com/zubyj/leetcode-explained',
25+
'X-Title': 'Leetcode Explained'
26+
},
27+
body: JSON.stringify({
28+
model: this.model,
29+
messages: [{
30+
role: 'user',
31+
content: params.prompt
32+
}],
33+
stream: false
34+
})
35+
});
36+
37+
if (!response.ok) {
38+
throw new Error(`OpenRouter API error: ${response.status}`);
39+
}
40+
41+
const data = await response.json();
42+
if (data.choices && data.choices[0]) {
43+
params.onEvent({
44+
type: 'answer',
45+
data: { text: data.choices[0].message.content }
46+
});
47+
params.onEvent({ type: 'done' });
48+
}
49+
} catch (error) {
50+
console.error('OpenRouter API error:', error);
51+
throw error;
52+
}
53+
}
54+
}

src/popup/popup.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,39 @@ a:hover {
203203

204204
#review {
205205
padding-top: 10px;
206+
}
207+
208+
/* Add styles for the API key section */
209+
.api-key-container {
210+
display: flex;
211+
gap: 10px;
212+
margin: 10px 0;
213+
}
214+
215+
#api-key-input {
216+
flex: 1;
217+
padding: 8px;
218+
border: 1px solid var(--border-color);
219+
border-radius: 4px;
220+
background-color: var(--primary-bg-color);
221+
color: var(--primary-text-color);
222+
}
223+
224+
#api-key-message {
225+
margin: 5px 0;
226+
font-size: 14px;
227+
}
228+
229+
.help-text {
230+
font-size: 12px;
231+
color: var(--secondary-text-color);
232+
}
233+
234+
.help-text a {
235+
color: var(--link-color);
236+
text-decoration: none;
237+
}
238+
239+
.help-text a:hover {
240+
text-decoration: underline;
206241
}

src/popup/popup.ts

Lines changed: 59 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ Creates the GPT buttons, sets the prompts, and displays the responses.
44
The user can also copy the code to their clipboard, clear the code, and open the settings page.
55
*/
66

7-
import {
8-
getChatGPTAccessToken,
9-
ChatGPTProvider,
10-
} from '../background/chatgpt/chatgpt.js';
11-
12-
import { initializeTheme, toggleTheme } from '../utils/theme.js';
7+
import { initializeTheme } from '../utils/theme.js';
8+
import { OpenRouterProvider } from '../background/openrouter/openrouter.js';
9+
import { ChatGPTProvider } from '../background/chatgpt/chatgpt.js';
10+
11+
// Add interface for ChatGPTProvider at the top level
12+
interface AIProvider {
13+
generateAnswer(params: {
14+
prompt: string,
15+
onEvent: (arg: { type: string, data?: { text: string } }) => void
16+
}): Promise<void>;
17+
}
1318

1419
/* Element selectors */
1520
const selectors: { [key: string]: string } = {
@@ -75,7 +80,7 @@ function setInfoMessage(message: string, duration: number) {
7580
}, duration);
7681
}
7782

78-
function initActionButton(buttonId: string, action: string, chatGPTProvider: ChatGPTProvider): void {
83+
function initActionButton(buttonId: string, action: string, chatGPTProvider: AIProvider): void {
7984
const actionButton = document.getElementById(buttonId);
8085
if (!actionButton) return;
8186
actionButton.onclick = async () => {
@@ -120,7 +125,7 @@ function formatResponseText(text: string): string {
120125
}
121126

122127
function processCode(
123-
chatGPTProvider: ChatGPTProvider,
128+
chatGPTProvider: AIProvider,
124129
codeText: string,
125130
action: string,
126131
): void {
@@ -200,64 +205,30 @@ function processCode(
200205
});
201206
}
202207

203-
async function main(): Promise<void> {
204-
205-
initializeTheme();
206-
207-
await Promise.all([
208+
async function loadStoredData(): Promise<void> {
209+
const [analyzeCodeResponseStored, fixCodeResponseStored, lastAction] = await Promise.all([
208210
getFromStorage(storageKeys.analyzeCodeResponse),
209211
getFromStorage(storageKeys.fixCodeResponse),
210212
getFromStorage(storageKeys.lastAction),
211-
getFromStorage(storageKeys.language),
212-
]);
213-
214-
// Load font size from storage
215-
let fontSizeElement = document.documentElement; // Or any specific element you want to change the font size of
216-
chrome.storage.local.get('fontSize', function (data) {
217-
if (data.fontSize) {
218-
fontSizeElement.style.setProperty('--dynamic-font-size', `${data.fontSize}px`);
219-
if (parseInt(data.fontSize) >= 18) {
220-
const width = (parseInt(data.fontSize) * 24 + 200);
221-
document.body.style.width = `${width + 20} px`;
222-
fixCodeContainer && (fixCodeContainer.style.maxWidth = `${width} px`);
223-
analyzeCodeResponse && (analyzeCodeResponse.style.maxWidth = `${width}px`);
224-
}
225-
226-
const sizes = document.getElementsByClassName('material-button');
227-
for (let i = 0; i < sizes.length; i++) {
228-
(sizes[i] as HTMLElement).style.width = `${data.fontSize * 13} px`;
229-
}
230-
}
231-
});
213+
]) as [string, string, string];
232214

233-
chrome.storage.local.get('analyzeCodeResponse', function (data) {
234-
if (data.analyzeCodeResponse) {
235-
analyzeCodeResponse && (analyzeCodeResponse.innerHTML = data.analyzeCodeResponse);
236-
(window as any).Prism.highlightAll();
237-
}
238-
});
215+
if (analyzeCodeResponseStored && lastAction === 'analyze') {
216+
analyzeCodeResponse && (analyzeCodeResponse.innerHTML = analyzeCodeResponseStored);
217+
analyzeCodeResponse?.classList.remove('hidden');
218+
}
239219

240-
chrome.storage.local.get('fixCodeResponse', function (data) {
241-
if (data.fixCodeResponse) {
242-
fixCodeResponse && (fixCodeResponse.textContent = data.fixCodeResponse);
243-
(window as any).Prism.highlightAll();
244-
}
245-
});
220+
if (fixCodeResponseStored && lastAction === 'fix') {
221+
fixCodeResponse && (fixCodeResponse.textContent = fixCodeResponseStored);
222+
fixCodeContainer?.classList.remove('hidden');
223+
(window as any).Prism.highlightAll();
224+
}
225+
}
246226

247-
chrome.storage.local.get('lastAction', function (data) {
248-
if (data.lastAction) {
249-
if (data.lastAction === 'analyze') {
250-
analyzeCodeResponse && analyzeCodeResponse.classList.remove('hidden');
251-
fixCodeContainer && fixCodeContainer.classList.add('hidden');
252-
}
253-
else if (data.lastAction === 'fix') {
254-
analyzeCodeResponse && analyzeCodeResponse.classList.add('hidden');
255-
fixCodeContainer && fixCodeContainer.classList.remove('hidden');
256-
}
257-
}
258-
});
227+
async function main(): Promise<void> {
228+
initializeTheme();
229+
await loadStoredData();
259230

260-
// get name of current tab and set info message to it if its a leetcode problem
231+
// get name of current tab and set info message
261232
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
262233
const tab = tabs[0];
263234
if (tab.url && tab.url.includes('leetcode.com/problems')) {
@@ -273,25 +244,39 @@ async function main(): Promise<void> {
273244
});
274245

275246
try {
276-
const accessToken = await getChatGPTAccessToken();
277-
if (accessToken) {
278-
const chatGPTProvider = new ChatGPTProvider(accessToken);
279-
initActionButton('get-complexity-btn', 'analyze', chatGPTProvider);
280-
initActionButton('fix-code-btn', 'fix', chatGPTProvider);
281-
initCopyButton();
282-
initClearButton();
283-
elements['getComplexityBtn'] && elements['getComplexityBtn'].classList.remove('hidden');
284-
elements['fixCodeBtn'] && elements['fixCodeBtn'].classList.remove('hidden');
247+
// Get OpenRouter API key from storage
248+
const data = await chrome.storage.local.get('openRouterApiKey');
249+
if (!data.openRouterApiKey) {
250+
displayApiKeyMessage();
251+
return;
285252
}
286-
else {
287-
displayLoginMessage();
253+
254+
// Verify API key is not empty string
255+
if (data.openRouterApiKey.trim() === '') {
256+
displayApiKeyMessage();
257+
return;
288258
}
289-
}
290-
catch (error) {
259+
260+
const openRouterProvider = new OpenRouterProvider(data.openRouterApiKey);
261+
initActionButton('get-complexity-btn', 'analyze', openRouterProvider);
262+
initActionButton('fix-code-btn', 'fix', openRouterProvider);
263+
initCopyButton();
264+
initClearButton();
265+
elements['getComplexityBtn']?.classList.remove('hidden');
266+
elements['fixCodeBtn']?.classList.remove('hidden');
267+
} catch (error) {
291268
handleError(error as Error);
292269
}
293270
}
294271

272+
function displayApiKeyMessage(): void {
273+
elements['loginBtn']?.classList.remove('hidden');
274+
if (infoMessage) {
275+
infoMessage.textContent = 'Please add your OpenRouter API key in settings';
276+
}
277+
disableAllButtons(true);
278+
}
279+
295280
function initCopyButton(): void {
296281
const copyButton = elements['copyCodeBtn'];
297282
if (!copyButton) return;
@@ -348,9 +333,9 @@ chrome.runtime.onMessage.addListener((message) => {
348333
});
349334

350335
/* Utility functions */
351-
function getFromStorage(key: string) {
336+
function getFromStorage(key: string): Promise<string> {
352337
return new Promise((resolve) => {
353-
chrome.storage.local.get(key, (data) => resolve(data[key]));
338+
chrome.storage.local.get(key, (data) => resolve(data[key] || ''));
354339
});
355340
}
356341

src/popup/settings.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@
3434
<button id="show-rating-btn" class="material-button settings-btn">
3535
<span id="show-rating-icon"> </span> Show Rating
3636
</button>
37+
<div class="settings-section">
38+
<h3>OpenRouter API Key</h3>
39+
<div class="api-key-container">
40+
<input type="password" id="api-key-input" placeholder="Enter your OpenRouter API key">
41+
<button id="save-api-key-btn" class="material-button">Save</button>
42+
</div>
43+
<p id="api-key-message"></p>
44+
<p class="help-text">Get your API key from <a href="https://openrouter.ai/keys"
45+
target="_blank">openrouter.ai/keys</a></p>
46+
</div>
3747
<div id="review">
3848
Leave us a
3949
<a target="_blank"

0 commit comments

Comments
 (0)