While Builing a Chrome extension with OpenAi assistant API, i came across a method that returns a streamed response, but my code is just getting the first node of the response all the time, and the one i want is almost at the end with a “event”:”thread.message.completed” tag and a Content parameter
background.js
"use strict";
// On Chrome Install
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.local.set({ 'apiKey': 'sk-proj-XXX' }, () => {
console.log("API key has been stored.");
});
});
// Listen for the browser action click event to open the form in a new window
chrome.action.onClicked.addListener(() => {
chrome.windows.create({
url: chrome.runtime.getURL("popup.html"),
type: "popup",
width: 400,
height: 800
});
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "fetchAnswer") {
chrome.storage.local.get('apiKey', async (result) => {
if (result.apiKey) {
try {
const threadResponse = await fetch('https://api.openai.com/v1/threads', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + result.apiKey,
'OpenAI-Beta': 'assistants=v2'
},
});
if (!threadResponse.ok) {
throw new Error('Network response threads was not ok ' + threadResponse.statusText);
}
const threadData = await threadResponse.json();
const threadId = threadData.id;
const messageResponse = await fetch(`https://api.openai.com/v1/threads/${threadId}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + result.apiKey,
'OpenAI-Beta': 'assistants=v2'
},
body: JSON.stringify({
role: "user",
content: [
{
type: "text",
text: message.question
}
]
})
});
if (!messageResponse.ok) {
throw new Error('Network response messages was not ok ' + messageResponse.statusText);
}
const runResponse = await fetch(`https://api.openai.com/v1/threads/${threadId}/runs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + result.apiKey,
'OpenAI-Beta': 'assistants=v2'
},
body: JSON.stringify({
assistant_id: "asst_xxx"
})
});
if (!runResponse.ok) {
throw new Error('Network response runs was not ok ' + runResponse.statusText);
}
const reader = runResponse.body.getReader();
const stream = new ReadableStream({
start(controller) {
function push() {
reader.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
controller.enqueue(value);
push();
});
}
push();
}
});
const response = new Response(stream, {
headers: { 'Content-Type': 'application/json' }
});
const readerStream = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let resultStream = '';
while (true) {
const { done, value } = await readerStream.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let boundary = buffer.indexOf('nn');
while (boundary !== -1) {
const eventString = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
if (eventString.trim()) {
const event = JSON.parse(eventString.trim());
handleEvent(event);
}
boundary = buffer.indexOf('nn');
}
console.log("Boundary: " + boundary + 'n');
console.log("buffer: " + buffer + 'n');
resultStream += decoder.decode(value, { stream: true });
console.log(resultStream + 'n');
}
sendResponse({ text: resultStream });
/*
const readerStream = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { done, value } = await readerStream.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let boundary = buffer.indexOf('nn');
while (boundary !== -1) {
const eventString = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
if (eventString.trim()) {
const event = JSON.parse(eventString.trim());
handleEvent(event);
}
boundary = buffer.indexOf('nn');
}
}
sendResponse({ status: 'completed' });
*/
} catch (error) {
sendResponse({ error: error.message });
}
} else {
sendResponse({ error: 'API key not found.' });
}
});
return true; // Will respond asynchronously.
}
});
function handleEvent(event) {
switch (event.event) {
case 'thread.message.delta':
console.log('Text Delta:', event.data);
break;
case 'thread.message.completed':
console.log('Message Completed:', event.data);
const textContent = event.data.content.find(c => c.type === 'text').text.value;
chrome.runtime.sendMessage({ action: 'displayText', text: textContent });
break;
case 'thread.run.step.completed':
console.log('Step Completed:', event.data);
break;
case 'thread.run.completed':
console.log('Run Completed:', event.data);
break;
default:
console.log('Unknown event:', event);
}
}
manifest.json
{
"name": "Ensight Plus Helper",
"version": "1.0.0",
"manifest_version": 3,
"description": "Ask questions about the user guide",
"background": {
"service_worker": "background.js"
},
"permissions": [
"activeTab",
"storage",
"scripting"
],
"host_permissions": [
"https://api.openai.com/*"
],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
},
"icons": {
"48": "EnSightLogo.png"
},
"action": {
"default_icon": "EnSightLogo.png"
}
}
popup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script type="module" src="./popup.js"></script>
<title>EnsightPlus Guide</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f9;
color: #333;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
width: 400px;
}
h1 {
font-size: 2rem;
margin-bottom: 1rem;
}
#chatbox {
width: 80%;
max-width: 600px;
height: 400px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow-y: scroll;
margin-bottom: 1rem;
background-color: #fff;
}
.message {
margin-bottom: 1rem;
}
.message.user {
text-align: right;
color: #4a90e2;
}
.message.bot {
text-align: left;
color: #333;
}
textarea {
width: 80%;
max-width: 600px;
height: 50px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 1rem;
resize: none;
margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
button {
padding: 10px 20px;
font-size: 1rem;
border: none;
border-radius: 5px;
background-color: #ea580b;
color: white;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #ef7c3b;
}
</style>
</head>
<body>
<h1>What do you need help with today?</h1>
<div id="chatbox"></div>
<textarea id="question" name="ensightgpt" placeholder="Enter your message here."></textarea>
<button id="askButton">Send</button>
</body>
</html>
popup.js
document.addEventListener('DOMContentLoaded', () => {
const askButton = document.getElementById('askButton');
const questionInput = document.getElementById('question');
const chatbox = document.getElementById('chatbox');
askButton.addEventListener('click', () => {
const question = questionInput.value;
if (question.trim() === "") return;
addMessageToChatbox(question, 'user');
questionInput.value = '';
chrome.runtime.sendMessage({ action: "fetchAnswer", question: question }, (response) => {
if (response.error) {
addMessageToChatbox('Error: ' + response.error, 'bot');
} else {
addMessageToChatbox(response.text, 'bot');
}
});
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'displayText') {
addMessageToChatbox(request.text, 'bot');
}
});
function addMessageToChatbox(message, sender) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', sender);
messageDiv.textContent = message;
chatbox.appendChild(messageDiv);
chatbox.scrollTop = chatbox.scrollHeight;
}
});