Skip to content

Commit

Permalink
Merge pull request #10 from ivanyu/openai
Browse files Browse the repository at this point in the history
Support OpenAI
  • Loading branch information
ivanyu authored Dec 8, 2024
2 parents b028c8b + 5f60f4b commit 307fd33
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 89 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ build-firefox: dist-firefox

dist-chrome: dist-firefox
cp -r dist-firefox dist-chrome
jq '.background = {"service_worker": .background.scripts[1]}' < dist-chrome/manifest.json > dist-chrome/manifest-tmp.json
jq '.background = {"service_worker": .background.scripts[-1]}' < dist-chrome/manifest.json > dist-chrome/manifest-tmp.json
mv dist-chrome/manifest-tmp.json dist-chrome/manifest.json

.PHONY: build-chrome
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Hacker News TL;DR

A bring-your-own-key browser extension for summarizing Hacker News articles with LLMs.
A bring-your-own-key browser extension for summarizing Hacker News articles with OpenAI and Anthropic LLMs.

The extension will add the summarize buttons to the HN front page and article pages. Just provide your Anthropic (or soon OpenAI) API key and you're good to go.
The extension will add the summarize buttons to the HN front page and article pages. Just provide your Anthropic or OpenAI API key and you're good to go.

![Screenshot 1](screen1.png)

Expand Down
3 changes: 2 additions & 1 deletion manifest-firefox.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Hacker News TL;DR",
"version": "0.1.0",
"description": "A bring-your-own-key extension for summarizing Hacker News articles with LLMs.",
"description": "A bring-your-own-key extension for summarizing Hacker News articles with OpenAI and Anthropic LLMs.",
"icons": {
"16": "icon16.png",
"32": "icon32.png",
Expand Down Expand Up @@ -39,6 +39,7 @@
"background": {
"scripts": [
"browser-polyfill.js",
"options_const.js",
"background.js"
]
}
Expand Down
141 changes: 96 additions & 45 deletions src/background.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
try {
// Load `browser-polyfill.js` for Chrome.
// Load for Chrome.
importScripts("browser-polyfill.js");
importScripts("options_const.js");
} catch (e) {
if (e instanceof ReferenceError) {
// It's probably Firefox, where `browser-polyfill.js` is provided through `background.scripts`.
// It's probably Firefox, where necessary scripts are provided through `background.scripts`.
// Do nothing.
} else {
throw e;
Expand All @@ -26,8 +27,6 @@ async function onActionClicked() {
browser.action.onClicked.addListener(onActionClicked);

async function summarize(url) {
// return{"summary":["This page is a detailed tutorial about installing Docker natively on an Android phone (specifically a OnePlus 6T) and using it as a home server. The guide walks through the following key steps:\n\n1. Preparing the device by enabling developer mode and USB debugging\n2. Installing Fastboot on a PC\n3. Downloading PostmarketOS files\n4. Entering Fastboot mode\n5. Flashing PostmarketOS onto the phone\n6. Setting up SSH\n7. Installing Docker on the Android phone\n8. Running Docker containers (with Portainer as an example)\n\nThe tutorial provides step-by-step instructions with commands and explanations, highlighting the potential of repurposing an old Android phone as a functional home server. It also notes some limitations, such as relying on Wi-Fi and having limited storage. The author suggests this method can be an alternative to using a Raspberry Pi, with the added benefits of an integrated screen and battery."],"model":"claude-3-5-haiku-20241022","input_tokens":62854,"output_tokens":211}

var content;
try {
const response = await fetch(url);
Expand All @@ -45,58 +44,110 @@ async function summarize(url) {

async function getSummary(pageContent) {
const options = await browser.storage.sync.get([
'anthropic.model',
'anthropic.api_key',
PROVIDER_CONF,
OPENAI_API_KEY_CONF,
OPENAI_MODEL_CONF,
ANTHROPIC_API_KEY_CONF,
ANTHROPIC_MODEL_CONF,
]);

const anthropicApiKey = options['anthropic.api_key'];
if (!anthropicApiKey) {
throw new Error('Anthropic API key not found');
}
const anthropicModel = options['anthropic.model'] || 'claude-3-5-haiku-20241022';

var url = "";
const headers = {
'content-type': 'application/json'
};
const requestContent = [
{"type": "text", "text": "I want you to summarize the following HTML body:"},
{"type": "text", "text": pageContent},
{"type": "text", "text": "Please return only the summary, no other text or comments. Do not call it 'HTML body', but 'page'."},
{ "type": "text", "text": "I want you to summarize the following HTML body:" },
{ "type": "text", "text": pageContent },
{ "type": "text", "text": "Please return only the summary, no other text or comments. Do not call it 'HTML body', but 'page'." },
];
const response = await fetch('https://api.anthropic.com/v1/messages', {
const body = {
"messages": [{ "role": "user", "content": requestContent }],
"temperature": 0.5
};

var model = "";

const maxTokens = 1000;
const systemPrompt = "You are a helpful and attentive assistant that summarizes web pages.";

const provider = options[PROVIDER_CONF] || DEFAULT_PROVIDER;
switch (provider) {
case OPENAI_PROVIDER:
url = 'https://api.openai.com/v1/chat/completions';

const openaiApiKey = options[OPENAI_API_KEY_CONF];
if (!openaiApiKey) {
throw new Error('OpenAI API key not found');
}
const openaiModel = options[OPENAI_MODEL_CONF] || DEFAULT_OPENAI_MODEL;

headers['Authorization'] = 'Bearer ' + openaiApiKey;

model = openaiModel;
body['messages'].unshift({ "role": "system", "content": systemPrompt });
body['max_completion_tokens'] = maxTokens;
break;

case ANTHROPIC_PROVIDER:
url = 'https://api.anthropic.com/v1/messages';

const anthropicApiKey = options[ANTHROPIC_API_KEY_CONF];
if (!anthropicApiKey) {
throw new Error('Anthropic API key not found');
}
const anthropicModel = options[ANTHROPIC_MODEL_CONF] || DEFAULT_ANTHROPIC_MODEL;

headers['x-api-key'] = anthropicApiKey;
headers['anthropic-version'] = '2023-06-01';
headers['anthropic-dangerous-direct-browser-access'] = 'true';

model = anthropicModel;
body['system'] = systemPrompt;
body['max_tokens'] = maxTokens;
break;

default:
throw new Error('Unknown provider: ' + options[PROVIDER_CONF]);
}
body['model'] = model;

const response = await fetch(url, {
method: 'POST',
headers: {
'x-api-key': anthropicApiKey,
'anthropic-version': '2023-06-01',
'content-type': 'application/json',
'anthropic-dangerous-direct-browser-access': 'true'
},
body: JSON.stringify({
"model": anthropicModel,
"max_tokens": 1000,
"temperature": 0.5,
"system": "You are a helpful and attentive assistant that summarizes web pages.",
"messages": [{"role": "user", "content": requestContent}]
})
headers: headers,
body: JSON.stringify(body)
});

const responseJson = await response.json();
if (responseJson.type === "error") {
if (!response.ok) {
throw new Error(JSON.stringify(responseJson.error));
}

const summary = [];
for (const message of responseJson.content) {
if (message.type === "text") {
summary.push(message.text);
} else {
summary.push("Unknown message type: " + message.type);
}
}

return {
"summary": summary,
"model": anthropicModel,
"input_tokens": responseJson.usage.input_tokens,
"output_tokens": responseJson.usage.output_tokens

const result = {
'summary': [],
'model': model,
'input_tokens': 0,
'output_tokens': 0
};
switch (provider) {
case OPENAI_PROVIDER:
result['summary'].push(responseJson.choices[0].message.content);
result['input_tokens'] = responseJson.usage.prompt_tokens;
result['output_tokens'] = responseJson.usage.completion_tokens;
break;

case ANTHROPIC_PROVIDER:
for (const message of responseJson.content) {
if (message.type === 'text') {
result['summary'].push(message.text);
} else {
result['summary'].push('Unknown message type: ' + message.type);
}
}
result['input_tokens'] = responseJson.usage.input_tokens;
result['output_tokens'] = responseJson.usage.output_tokens;
break;
}
return result;
}

browser.runtime.onMessage.addListener(function (message, sender, senderResponse) {
Expand Down
94 changes: 62 additions & 32 deletions src/options.html
Original file line number Diff line number Diff line change
@@ -1,48 +1,78 @@
<!DOCTYPE html>

<html>

<head>
<meta charset="utf-8">
<meta name="color-scheme" content="dark light">

<style>
.options-group {
border: 1px solid #ccc;
padding: 0.5em 0.5em;
margin-bottom: -1px;
}

.options-row {
display: flex;
align-items: center;
gap: 0.5em;
margin: 1em 0;
}

.options-row-input {
flex: 0.9;
}
</style>
</head>

<body>
<form id="optionsForm">
<table>
<tr>
<td>
<input type="radio" id="model-provider-openai" name="model-provider" value="model-provider-openai" disabled>
<label for="model-provider-openai" disabled>OpenAI (coming soon)</label>
</td>
</tr>

<tr>
<td>
<input type="radio" id="model-provider-anthropic" name="model-provider" value="model-provider-anthropic" checked>
<label for="model-provider-anthropic">Anthropic</label>
</td>
</tr>

<tr>
<td>
<label for="anthropic-api-key">Anthropic API Key: </label>
<input type="password" id="anthropic-api-key">
</td>
</tr>

<tr>
<td>
<label for="anthropic-model">Model: </label>
<select id="anthropic-model" name="anthropic-model">
<option value="claude-3-5-haiku-20241022">Claude 3.5 Haiku</option>
<option value="claude-3-5-sonnet-20241022">Claude 3.5 Sonnet</option>
</select>
</td>
</tr>
</table>
<div id="openai-options" class="options-group">
<div class="options-row">
<input type="radio" id="provider-openai" name="provider" value="openai" checked>
<label for="provider-openai">OpenAI</label>
</div>

<div class="options-row">
<label for="openai-api-key">API Key: </label>
<input type="password" id="openai-api-key" class="options-row-input">
</div>

<div class="options-row">
<label for="openai-model">Model: </label>
<select id="openai-model" name="openai-model" style="flex: 0.3;">
<option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
<option value="gpt-4">gpt-4</option>
<option value="gpt-4o-mini">gpt-4o-mini</option>
<option value="gpt-4o">gpt-4o</option>
</select>
</div>
</div>

<div id="anthropic-options" class="options-group">
<div class="options-row">
<input type="radio" id="provider-anthropic" name="provider" value="anthropic">
<label for="provider-anthropic">Anthropic</label>
</div>

<div class="options-row">
<label for="anthropic-api-key">API Key: </label>
<input type="password" id="anthropic-api-key" class="options-row-input">
</div>

<div class="options-row">
<label for="anthropic-model">Model: </label>
<select id="anthropic-model" name="anthropic-model">
<option value="claude-3-5-haiku-20241022">claude-3-5-haiku-20241022</option>
<option value="claude-3-5-sonnet-20241022">claude-3-5-sonnet-20241022</option>
</select>
</div>
</div>
</form>

<script src="browser-polyfill.js"></script>
<script src="options_const.js"></script>
<script src="options.js"></script>
</body>

Expand Down
Loading

0 comments on commit 307fd33

Please sign in to comment.