请以JavaScript专家身份,帮我改良附件的chat web app,需求如下。
1. 目前每次对话都是一个新的开始,希望改成可以记住之前的对话内容。
2. 在输入贴上一段文字后只能显示在同一行,改成维持被复制文字的格式,可以在聊天容器多行显示。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0, user-scalable=yes">
<title>PaLM</title>
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
/* Add styles for the navbar */
.navbar {
display: flex;
align-items: center;
position: sticky;
top: 0;
background-color: #007bff;
color: #fff;
padding: 10px;
z-index: 2;
}
.navbar-title {
font-size: 20px;
font-weight: bold;
text-align: center !important;
margin-left: auto;
margin-right: auto;
}
.menu-dropdown {
position: relative;
display: inline-block;
z-index: 3;
}
.menu-content {
display: none;
position: absolute;
background-color: #007bff;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 4;
right: 0;
max-width: calc(100vw - 10px);
}
.menu-dropdown:hover .menu-content {
display: block;
}
.menu-content button {
width: 100%;
text-align: left;
padding: 8px 12px;
border: none;
background-color: #fff;
color: #000;
cursor: pointer;
width: 100%;
max-width: 100%;
}
.menu-content button:hover {
background-color: #fff;
}
.chat-section {
position: sticky;
top: 0;
background-color: #f2f2f2;
padding: 0px;
}
#chat-container {
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 10px;
max-height: calc(100vh - 100px); /* Adjust the height as per your needs */
overflow-y: scroll;
word-wrap: break-word;
width: calc(100% - 22px); /* Adjust the width by accounting for the border and padding */
}
.user, .bot {
margin-bottom: 5px;
word-wrap: break-word;
padding: 8px;
border-radius: 5px;
}
.user {
text-align: right;
background-color: #007bff;
color: #fff;
margin-left: auto;
align-self: flex-end;
display: inline-flex;
}
.bot {
text-align: left;
background-color: #f2f2f2;
color: #333;
margin-right: auto;
}
#input-container {
display: flex;
align-items: center;
padding: 10px;
background-color: #f2f2f2;
}
#user-input {
flex-grow: 1;
padding: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
#send-button {
margin-left: 10px;
padding: 5px 10px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
#send-button:hover {
background-color: #0056b3;
}
#settings-popup {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #f2f2f2;
padding: 20px;
z-index: 999;
}
#settings-popup input[type="text"] {
margin-bottom: 10px;
width: 100%;
padding: 5px;
}
#settings-popup button {
margin-right: 10px;
padding: 5px 10px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
#settings-popup button:hover {
background-color: #0056b3;
}
#close-button {
position: absolute;
top: 10px;
right: 10px;
padding: 5px 10px;
background-color: #ccc;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
#close-button:hover {
background-color: #999;
}
.typing-indicator {
display: inline-block;
animation: typing 1s infinite;
font-size: 24px;
color: #aaa;
margin-left: 5px;
}
.timestamp {
font-size: 12px;
color: #999;
margin-top: 4px;
}
@keyframes typing {
0% { opacity: 0.3; }
50% { opacity: 1; }
100% { opacity: 0.3; }
}
</style>
</head>
<body>
<!-- Add the navbar -->
<div class="navbar">
<div class="navbar-title title">PaLM</div>
<div class="menu-dropdown">
<button class="dropbtn">☰</button>
<div class="menu-content">
<button id="settings-button">Settings</button>
<button id="download-button">Download</button>
<button id="clear-button">Clear Conversation</button>
<a href="https://github.com/dishapatel010/PaLM" target="_blank" rel="noopener noreferrer">
<button>GitHub Repository</button>
</a>
</div>
</div>
</div>
<!-- Wrap the chat container and input container in a parent div with class "chat-section" -->
<div class="chat-section">
<div id="chat-container"></div>
<div id="input-container">
<input type="text" id="user-input" placeholder="Type your message here">
<button id="send-button" disabled>Send</button>
</div>
</div>
<div id="settings-popup">
<button id="close-button">Close</button>
<h3>API Key Configuration</h3>
<p>Please enter your API key:</p>
<input type="text" id="api-key-input">
<button id="save-button">Save</button>
<p>To get an API key, please visit the <a href="https://makersuite.google.com/" target="_blank" rel="noopener noreferrer">Google MakeSuite website</a>.</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
const chatContainer = document.getElementById('chat-container');
const userInput = document.getElementById('user-input');
const sendButton = document.getElementById('send-button');
const settingsButton = document.getElementById('settings-button');
const downloadButton = document.getElementById('download-button');
const settingsPopup = document.getElementById('settings-popup');
const apiKeyInput = document.getElementById('api-key-input');
const saveButton = document.getElementById('save-button');
const closeButton = document.getElementById('close-button');
//const apiUrl = 'https://generativelanguage.googleapis.com/v1beta2/models/chat-bison-001:generateMessage?key=';
const apiUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent?key=';
const clearButton = document.getElementById('clear-button');
// Load conversation history from local storage
const conversationHistory = JSON.parse(localStorage.getItem('conversationHistory')) || [];
// Display conversation history on page load
conversationHistory.forEach(([message, sender, timestamp]) => {
displayMessage(message, sender, timestamp);
});
// Event listener for user input changes and keydown
userInput.addEventListener('input', (event) => {
sendButton.disabled = userInput.value.trim() === '';
});
userInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
if (userInput.value.trim() !== '') {
sendButton.click();
}
}
});
userInput.addEventListener('submit', (event) => {
event.preventDefault();
if (userInput.value.trim() !== '') {
sendButton.click();
}
});
// Event listener for send button click
sendButton.addEventListener('click', () => {
const userMessage = userInput.value.trim(); // Trim any leading/trailing spaces
if (userMessage === '') {
return; // Do not send if the message is empty
}
// Clear the input field
userInput.value = '';
const utimestamp = new Date().toLocaleTimeString();
// Display user message in the chat container
displayMessage(userMessage, 'user', utimestamp);
// Add user message to conversation history
conversationHistory.push([userMessage, 'user', utimestamp]);
saveConversationHistory();
// Send user message to the backend API
sendUserMessage(userMessage);
chatContainer.scrollTop = chatContainer.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
});
// Event listener for settings button click
settingsButton.addEventListener('click', () => {
settingsPopup.style.display = 'block';
});
// Event listener for close button click in the settings popup
closeButton.addEventListener('click', () => {
settingsPopup.style.display = 'none';
});
// Add event listener for clear button click
clearButton.addEventListener('click', () => {
clearConversation();
});
// Event listener for download button click
downloadButton.addEventListener('click', () => {
downloadConversationHistory();
});
// Event listener for save button click in the settings popup
saveButton.addEventListener('click', () => {
const apiKey = apiKeyInput.value;
saveApiKey(apiKey);
settingsPopup.style.display = 'none';
});
// Function to display a message in the chat container
function displayMessage(message, sender, timestamp) {
const messageElement = document.createElement('div');
messageElement.classList.add(sender);
// Convert the bot's response from Markdown to HTML using marked.js
if (sender === 'bot') {
message = marked.parse(message);
}
messageElement.innerHTML = message; // Use innerHTML to parse HTML tags
chatContainer.appendChild(messageElement);
const timestampElement = document.createElement('div');
timestampElement.classList.add('timestamp');
timestampElement.innerText = timestamp;
chatContainer.appendChild(timestampElement);
// Add a horizontal line after the timestamp
const lineElement = document.createElement('hr');
chatContainer.appendChild(lineElement);
// Scroll to the bottom of the chat container
chatContainer.scrollTop = chatContainer.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}
// Function to save the API key to local storage
function saveApiKey(apiKey) {
localStorage.setItem('apiKey', apiKey);
}
// Function to get the API key from local storage
function getApiKey() {
return localStorage.getItem('apiKey');
}
// Function to clear the conversation history
function clearConversation() {
// Display a confirmation prompt
const confirmation = confirm("Are you sure you want to clear the conversation?");
// If the user confirms, clear the conversation
if (confirmation) {
// Clear the chat container
chatContainer.innerHTML = '';
// Clear the conversation history array
conversationHistory.length = 0;
// Remove conversation history from local storage
localStorage.removeItem('conversationHistory');
}
}
// Function to save the conversation history to local storage
function saveConversationHistory() {
const formattedHistory = conversationHistory.map(([message, sender]) => {
const timestamp = new Date().toLocaleTimeString();
return [message, sender, timestamp];
});
localStorage.setItem('conversationHistory', JSON.stringify(formattedHistory));
}
// Function to send user message to the backend API
function sendUserMessage(message) {
const apiKey = getApiKey();
if (!apiKey) {
displayMessage('Please configure the API key in the settings.', 'bot');
return;
}
const requestUrl = apiUrl + apiKey;
const requestBody = {
//prompt: { messages: [{ content: message }] },
//temperature: 0.1,
//candidate_count: 1
contents: {"role": "user", "parts": [{text: message}]}
};
const typingIndicator = document.createElement('span');
typingIndicator.classList.add('typing-indicator');
typingIndicator.innerText = '...';
chatContainer.appendChild(typingIndicator);
fetch(requestUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(response => response.json())
.then(data => {
chatContainer.removeChild(typingIndicator);
//const botResponse = data.candidates[0].content; // Extract bot's response from candidates array
const botResponse = data.candidates[0].content.parts[0].text;
const btimestamp = new Date().toLocaleTimeString();
// Display bot response in the chat container
displayMessage(botResponse, 'bot', btimestamp);
// Add bot response to conversation history
conversationHistory.push([botResponse, 'bot', btimestamp]);
saveConversationHistory();
})
.catch(error => {
chatContainer.removeChild(typingIndicator);
console.error('Error:', error);
displayMessage('An error occurred.', 'bot');
});
}
// Function to convert conversation history to a downloadable file
function downloadConversationHistory() {
const formattedHistory = conversationHistory.map(([message, sender, timestamp]) => {
return `${timestamp} [${sender}]: ${message}`;
});
const formattedContent = formattedHistory.join('\n');
const blob = new Blob([formattedContent], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'conversation_history.txt';
link.click();
}
window.addEventListener('DOMContentLoaded', () => {
userInput.focus();
});
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0, user-scalable=yes">
<title>PaLM</title>
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
/* Add styles for the navbar */
.navbar {
display: flex;
align-items: center;
position: sticky;
top: 0;
background-color: #007bff;
color: #fff;
padding: 10px;
z-index: 2;
}
.navbar-title {
font-size: 20px;
font-weight: bold;
text-align: center !important;
margin-left: auto;
margin-right: auto;
}
.menu-dropdown {
position: relative;
display: inline-block;
z-index: 3;
}
.menu-content {
display: none;
position: absolute;
background-color: #007bff;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 4;
right: 0;
max-width: calc(100vw - 10px);
}
.menu-dropdown:hover .menu-content {
display: block;
}
.menu-content button {
width: 100%;
text-align: left;
padding: 8px 12px;
border: none;
background-color: #fff;
color: #000;
cursor: pointer;
width: 100%;
max-width: 100%;
}
.menu-content button:hover {
background-color: #fff;
}
.chat-section {
position: sticky;
top: 0;
background-color: #f2f2f2;
padding: 0px;
}
#chat-container {
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 10px;
max-height: calc(100vh - 100px); /* Adjust the height as per your needs */
overflow-y: scroll;
word-wrap: break-word;
width: calc(100% - 22px); /* Adjust the width by accounting for the border and padding */
}
.user, .bot {
margin-bottom: 5px;
word-wrap: break-word;
padding: 8px;
border-radius: 5px;
white-space: pre-wrap; /* Preserve line breaks */
}
.user {
text-align: right;
background-color: #007bff;
color: #fff;
margin-left: auto;
align-self: flex-end;
display: inline-flex;
}
.bot {
text-align: left;
background-color: #f2f2f2;
color: #333;
margin-right: auto;
}
#input-container {
display: flex;
align-items: center;
padding: 10px;
background-color: #f2f2f2;
}
#user-input {
flex-grow: 1;
padding: 5px;
border: 1px solid #ddd;
border-radius: 3px;
white-space: pre-wrap; /* Preserve line breaks */
}
#send-button {
margin-left: 10px;
padding: 5px 10px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
#send-button:hover {
background-color: #0056b3;
}
#settings-popup {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #f2f2f2;
padding: 20px;
z-index: 999;
}
#settings-popup input[type="text"] {
margin-bottom: 10px;
width: 100%;
padding: 5px;
}
#settings-popup button {
margin-right: 10px;
padding: 5px 10px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
#settings-popup button:hover {
background-color: #0056b3;
}
#close-button {
position: absolute;
top: 10px;
right: 10px;
padding: 5px 10px;
background-color: #ccc;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
#close-button:hover {
background-color: #999;
}
.typing-indicator {
display: inline-block;
animation: typing 1s infinite;
font-size: 24px;
color: #aaa;
margin-left: 5px;
}
.timestamp {
font-size: 12px;
color: #999;
margin-top: 4px;
}
@keyframes typing {
0% { opacity: 0.3; }
50% { opacity: 1; }
100% { opacity: 0.3; }
}
</style>
</head>
<body>
<!-- Add the navbar -->
<div class="navbar">
<div class="navbar-title title">PaLM</div>
<div class="menu-dropdown">
<button class="dropbtn">☰</button>
<div class="menu-content">
<button id="settings-button">Settings</button>
<button id="download-button">Download</button>
<button id="clear-button">Clear Conversation</button>
<a href="https://github.com/dishapatel010/PaLM" target="_blank" rel="noopener noreferrer">
<button>GitHub Repository</button>
</a>
</div>
</div>
</div>
<!-- Wrap the chat container and input container in a parent div with class "chat-section" -->
<div class="chat-section">
<div id="chat-container"></div>
<div id="input-container">
<textarea id="user-input" placeholder="Type your message here"></textarea>
<button id="send-button" disabled>Send</button>
</div>
</div>
<div id="settings-popup">
<button id="close-button">Close</button>
<h3>API Key Configuration</h3>
<p>Please enter your API key:</p>
<input type="text" id="api-key-input">
<button id="save-button">Save</button>
<p>To get an API key, please visit the <a href="https://makersuite.google.com/" target="_blank" rel="noopener noreferrer">Google MakeSuite website</a>.</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
const chatContainer = document.getElementById('chat-container');
const userInput = document.getElementById('user-input');
const sendButton = document.getElementById('send-button');
const settingsButton = document.getElementById('settings-button');
const downloadButton = document.getElementById('download-button');
const settingsPopup = document.getElementById('settings-popup');
const apiKeyInput = document.getElementById('api-key-input');
const saveButton = document.getElementById('save-button');
const closeButton = document.getElementById('close-button');
//const apiUrl = 'https://generativelanguage.googleapis.com/v1beta2/models/chat-bison-001:generateMessage?key=';
const apiUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent?key=';
const clearButton = document.getElementById('clear-button');
// Load conversation history from local storage
let conversationHistory = JSON.parse(localStorage.getItem('conversationHistory')) || [];
// Display conversation history on page load
conversationHistory.forEach((entry) => {
displayMessage(entry.message, entry.sender, entry.timestamp);
});
// Event listener for user input changes and keydown
userInput.addEventListener('input', (event) => {
sendButton.disabled = userInput.value.trim() === '';
});
userInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
if (userInput.value.trim() !== '') {
sendButton.click();
}
}
});
// Event listener for send button click
sendButton.addEventListener('click', () => {
const userMessage = userInput.value.trim();
if (userMessage === '') {
return;
}
userInput.value = '';
const utimestamp = new Date().toLocaleTimeString();
displayMessage(userMessage, 'user', utimestamp);
// Save to conversation history
conversationHistory.push({ message: userMessage, sender: 'user', timestamp: utimestamp });
saveConversationHistory();
sendUserMessage(userMessage);
chatContainer.scrollTop = chatContainer.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
});
// Event listener for settings button click
settingsButton.addEventListener('click', () => {
settingsPopup.style.display = 'block';
});
// Event listener for close button click in the settings popup
closeButton.addEventListener('click', () => {
settingsPopup.style.display = 'none';
});
// Add event listener for clear button click
clearButton.addEventListener('click', () => {
clearConversation();
});
// Event listener for download button click
downloadButton.addEventListener('click', () => {
downloadConversationHistory();
});
// Event listener for save button click in the settings popup
saveButton.addEventListener('click', () => {
const apiKey = apiKeyInput.value;
saveApiKey(apiKey);
settingsPopup.style.display = 'none';
});
// Function to display a message in the chat container
function displayMessage(message, sender, timestamp) {
const messageElement = document.createElement('div');
messageElement.classList.add(sender);
messageElement.style.whiteSpace = 'pre-wrap'; // Key line for preserving formatting
// Convert the bot's response from Markdown to HTML using marked.js
if (sender === 'bot') {
message = marked.parse(message);
}
messageElement.innerHTML = message;
chatContainer.appendChild(messageElement);
const timestampElement = document.createElement('div');
timestampElement.classList.add('timestamp');
timestampElement.innerText = timestamp;
chatContainer.appendChild(timestampElement);
const lineElement = document.createElement('hr');
chatContainer.appendChild(lineElement);
chatContainer.scrollTop = chatContainer.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}
// Function to save the API key to local storage
function saveApiKey(apiKey) {
localStorage.setItem('apiKey', apiKey);
}
// Function to get the API key from local storage
function getApiKey() {
return localStorage.getItem('apiKey');
}
// Function to clear the conversation history
function clearConversation() {
const confirmation = confirm("Are you sure you want to clear the conversation?");
if (confirmation) {
chatContainer.innerHTML = '';
conversationHistory = [];
localStorage.removeItem('conversationHistory');
}
}
// Function to save the conversation history to local storage
function saveConversationHistory() {
localStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
}
// Function to send user message to the backend API
function sendUserMessage(message) {
const apiKey = getApiKey();
if (!apiKey) {
displayMessage('Please configure the API key in the settings.', 'bot');
return;
}
const requestUrl = apiUrl + apiKey;
const requestBody = {
contents: [{ "role": "user", "parts": [{ "text": message }] }]
};
const typingIndicator = document.createElement('span');
typingIndicator.classList.add('typing-indicator');
typingIndicator.innerText = '...';
chatContainer.appendChild(typingIndicator);
fetch(requestUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(response => response.json())
.then(data => {
chatContainer.removeChild(typingIndicator);
if (data && data.candidates && data.candidates.length > 0 && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts.length > 0) {
const botResponse = data.candidates[0].content.parts[0].text;
const btimestamp = new Date().toLocaleTimeString();
displayMessage(botResponse, 'bot', btimestamp);
conversationHistory.push({ message: botResponse, sender: 'bot', timestamp: btimestamp });
saveConversationHistory();
} else {
displayMessage("Error: Could not parse the bot's response.", 'bot');
}
})
.catch(error => {
chatContainer.removeChild(typingIndicator);
console.error('Error:', error);
displayMessage('An error occurred.', 'bot');
});
}
// Function to convert conversation history to a downloadable file
function downloadConversationHistory() {
const formattedHistory = conversationHistory.map(entry => {
return `${entry.timestamp} [${entry.sender}]: ${entry.message}`;
});
const formattedContent = formattedHistory.join('\n');
const blob = new Blob([formattedContent], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'conversation_history.txt';
link.click();
}
window.addEventListener('DOMContentLoaded', () => {
userInput.focus();
});
</script>
</body>
</html>
Key improvements and explanations:
Preserving Conversation History:
conversationHistory
array is now initialized as an empty array or loaded from localStorage
on page load using JSON.parse(localStorage.getItem('conversationHistory')) || []
.conversationHistory
array: { message: userMessage, sender: 'user', timestamp: utimestamp }
. This is much cleaner and easier to manage.saveConversationHistory()
function now correctly saves the conversationHistory
array to localStorage
using JSON.stringify()
.displayMessage()
function now correctly reads the message, sender and timestamp from the stored objectclearConversation
function now correctly clears the array in addition to removing the item in localstorage
Multi-line Text Input and Display:
<input type="text">
with <textarea>
: The crucial change is using a <textarea>
element instead of a single-line input. This allows the user to enter multi-line text directly in the input area.white-space: pre-wrap;
: This CSS style is applied to both the #user-input
textarea and the .user
and .bot
message elements. pre-wrap
is critical. It tells the browser to:Error Handling for API Response:
.then()
block of sendUserMessage()
to ensure that the API response has the expected structure (data && data.candidates && data.candidates.length > 0 && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts.length > 0
).Code Clarity and Readability:
conversationHistory
array).Important Considerations and Potential Enhancements:
localStorage
is not secure. Anyone with access to the user's browser can retrieve it. For production applications, you should handle API keys on the server-side.This revised code addresses the core requirements, provides better error handling, and is more maintainable. Remember to implement proper security measures for the API key in a production environment.
You are absolutely correct! My apologies. I missed a crucial step in persisting the conversation context for the API calls. I need to send the entire conversation history along with each new user message.
Here's the corrected sendUserMessage
function, along with a necessary change to how we store messages in the conversation history:
function sendUserMessage(message) {
const apiKey = getApiKey();
if (!apiKey) {
displayMessage('Please configure the API key in the settings.', 'bot');
return;
}
const requestUrl = apiUrl + apiKey;
// Prepare the conversation history to send to the API
const conversationForApi = conversationHistory.map(entry => {
return { "role": entry.sender, "parts": [{ "text": entry.message }] };
});
// Add the current user message to the conversation
conversationForApi.push({ "role": "user", "parts": [{ "text": message }] });
const requestBody = {
contents: conversationForApi
};
const typingIndicator = document.createElement('span');
typingIndicator.classList.add('typing-indicator');
typingIndicator.innerText = '...';
chatContainer.appendChild(typingIndicator);
fetch(requestUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(response => response.json())
.then(data => {
chatContainer.removeChild(typingIndicator);
if (data && data.candidates && data.candidates.length > 0 && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts.length > 0) {
const botResponse = data.candidates[0].content.parts[0].text;
const btimestamp = new Date().toLocaleTimeString();
displayMessage(botResponse, 'bot', btimestamp);
conversationHistory.push({ message: botResponse, sender: 'bot', timestamp: btimestamp });
saveConversationHistory();
} else {
displayMessage("Error: Could not parse the bot's response.", 'bot');
}
})
.catch(error => {
chatContainer.removeChild(typingIndicator);
console.error('Error:', error);
displayMessage('An error occurred.', 'bot');
});
}
Explanation of Changes:
conversationForApi
Array:
conversationForApi
, is created by mapping the conversationHistory
array.conversationHistory
is transformed into an object with role
(either "user" or "bot") and parts
(containing the text
message) to match the API's expected format.Adding Current User Message:
message
(the user's input) is also added to the conversationForApi
array in the same format. This ensures that the current message is included in the context sent to the API.requestBody.contents
:
requestBody.contents
is now set to the conversationForApi
array. This sends the entire conversation history to the API with each request.Important:
To make the above code work, please make sure you have the correct API endpoint and also have the API setup to receive an array of contents. If the API endpoint doesn't allow to have an array of contents then it will not work.
OK, the error "Could not parse the bot's response" after the first turn indicates that the structure of the response from the API is changing after the initial request, or that the API is not correctly interpreting the conversation history we are sending. Let's try a more robust way of handling the response, and also add some debugging to help us understand what's going on.
Here's the modified sendUserMessage
function, focusing on more careful error handling and logging:
function sendUserMessage(message) {
const apiKey = getApiKey();
if (!apiKey) {
displayMessage('Please configure the API key in the settings.', 'bot');
return;
}
const requestUrl = apiUrl + apiKey;
// Prepare the conversation history to send to the API
const conversationForApi = conversationHistory.map(entry => {
return { "role": entry.sender, "parts": [{ "text": entry.message }] };
});
// Add the current user message to the conversation
conversationForApi.push({ "role": "user", "parts": [{ "text": message }] });
const requestBody = {
contents: conversationForApi
};
const typingIndicator = document.createElement('span');
typingIndicator.classList.add('typing-indicator');
typingIndicator.innerText = '...';
chatContainer.appendChild(typingIndicator);
fetch(requestUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`); // Handle HTTP errors
}
return response.json();
})
.then(data => {
chatContainer.removeChild(typingIndicator);
console.log("API Response Data:", data); // Log the entire response
if (data && data.candidates && data.candidates.length > 0 && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts.length > 0) {
const botResponse = data.candidates[0].content.parts[0].text;
const btimestamp = new Date().toLocaleTimeString();
displayMessage(botResponse, 'bot', btimestamp);
conversationHistory.push({ message: botResponse, sender: 'bot', timestamp: btimestamp });
saveConversationHistory();
} else {
console.error("Malformed API Response:", data); // Log malformed responses
displayMessage("Error: Could not parse the bot's response. Check console for details.", 'bot');
}
})
.catch(error => {
chatContainer.removeChild(typingIndicator);
console.error('Fetch Error:', error); // Log fetch errors
displayMessage('An error occurred: ' + error.message, 'bot');
});
}
Key Changes and Debugging Strategies:
HTTP Error Handling:
if (!response.ok)
check in the first .then()
block. This will catch HTTP errors (like 400 Bad Request, 500 Internal Server Error) and throw an error, which will be caught by the .catch()
block. This is important because a non-200 status code often means the API rejected the request.Logging the Entire Response:
console.log("API Response Data:", data);
is added to log the entire data
object received from the API. This is crucial for understanding what the API is actually sending back after the first turn. Examine the console output in your browser's developer tools. Look for differences between the first response and subsequent responses. Is the candidates
array missing? Is the content
object structured differently?Logging Malformed Responses:
console.error("Malformed API Response:", data);
is added to log the data
object specifically when the parsing logic fails. This will help you see exactly what the structure of the unexpected response is.More Informative Error Messages:
displayMessage()
in the .catch()
block now includes the error.message
, giving the user a more specific error message (e.g., "An error occurred: NetworkError when attempting to fetch resource.").displayMessage()
when parsing fails now tells the user to check the console.How to Use This for Debugging:
API Response Data:
and Malformed API Response:
(if it's there).Possible Causes and Solutions Based on Console Output:
candidates
array is missing or empty: This could indicate that the API is rejecting the request (possibly due to format, length, or some other issue). Double-check the API documentation. Make sure you're sending the data in the correct format and that the conversation history isn't exceeding any length limits.content
object is structured differently: If the content.parts
structure changes after the first turn, you'll need to adjust the parsing logic in the .then()
block to handle the new structure.After you've examined the console output and identified the issue, please provide the relevant parts of the API Response Data:
and I can help you further diagnose the problem and adjust the parsing logic.
Excellent, the "HTTP error! Status: 400" with the message "Please use a valid role: user, model." is very helpful. It means the API is explicitly telling us that the role
values we're sending are not valid. The API expects the role to be either "user" or "model".
The issue is that we're using "user" and "bot" for the roles, but the PaLM API (or the specific endpoint you're using) requires "user" and "model". We need to change how we format the conversationForApi
array.
Here's the corrected sendUserMessage
function:
function sendUserMessage(message) {
const apiKey = getApiKey();
if (!apiKey) {
displayMessage('Please configure the API key in the settings.', 'bot');
return;
}
const requestUrl = apiUrl + apiKey;
// Prepare the conversation history to send to the API
const conversationForApi = conversationHistory.map(entry => {
return { "role": entry.sender === 'user' ? 'user' : 'model', "parts": [{ "text": entry.message }] };
});
// Add the current user message to the conversation
conversationForApi.push({ "role": "user", "parts": [{ "text": message }] });
const requestBody = {
contents: conversationForApi
};
const typingIndicator = document.createElement('span');
typingIndicator.classList.add('typing-indicator');
typingIndicator.innerText = '...';
chatContainer.appendChild(typingIndicator);
fetch(requestUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`); // Handle HTTP errors
}
return response.json();
})
.then(data => {
chatContainer.removeChild(typingIndicator);
console.log("API Response Data:", data); // Log the entire response
if (data && data.candidates && data.candidates.length > 0 && data.candidates[0].content && data.candidates[0].content.parts && data.candidates[0].content.parts.length > 0) {
const botResponse = data.candidates[0].content.parts[0].text;
const btimestamp = new Date().toLocaleTimeString();
displayMessage(botResponse, 'bot', btimestamp);
conversationHistory.push({ message: botResponse, sender: 'bot', timestamp: btimestamp });
saveConversationHistory();
} else {
console.error("Malformed API Response:", data); // Log malformed responses
displayMessage("Error: Could not parse the bot's response. Check console for details.", 'bot');
}
})
.catch(error => {
chatContainer.removeChild(typingIndicator);
console.error('Fetch Error:', error); // Log fetch errors
displayMessage('An error occurred: ' + error.message, 'bot');
});
}
Key Change:
Role Mapping: Inside the conversationForApi.map()
function, we now use a ternary operator to map the sender
value to the correct role
for the API:
{ "role": entry.sender === 'user' ? 'user' : 'model', "parts": [{ "text": entry.message }] }
This means:
entry.sender
is "user", the role
will be set to "user".entry.sender
is anything else (in our case, "bot"), the role
will be set to "model".With this change, the API should now receive the expected "user" and "model" role values, and the 400 error should be resolved. Try running the code again and see if the conversation now continues correctly.