uncloseai.com/src/chat.js

287 lines
8 KiB
JavaScript

// Chat functionality and message handling
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
import hljs from "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/es/highlight.min.js";
import { API_KEY, getAPIConfig } from "./config.js";
import { getSelectedModel, getSelectedModelEndpoint } from "./models.js";
import { initializeChatHistory, saveConversationHistory } from "./storage.js";
import { generateTitleForTTS, speakText } from "./tts.js";
// Helper function to add copy buttons to code blocks
function addCodeBlockCopyButtons(element) {
const codeBlocks = element.querySelectorAll("pre code");
codeBlocks.forEach((codeBlock) => {
const pre = codeBlock.parentElement;
if (pre.tagName.toLowerCase() === "pre") {
// Check if copy button already exists to avoid duplicates
if (pre.querySelector('.code-copy-btn')) {
return;
}
// Make the pre element relative for positioning
pre.style.position = "relative";
// Create copy button
const copyBtn = document.createElement("button");
copyBtn.className = "uncloseai-code-copy-btn";
copyBtn.textContent = "📋";
copyBtn.title = "Copy code";
// Styling handled by CSS class
copyBtn.onclick = async () => {
try {
await navigator.clipboard.writeText(codeBlock.textContent);
copyBtn.textContent = "✓";
setTimeout(() => {
copyBtn.textContent = "📋";
}, 2000);
} catch (error) {
console.error("Failed to copy code:", error);
}
};
pre.appendChild(copyBtn);
}
});
}
// Initialize chat history
export let chatHistory = initializeChatHistory();
// Generator function to send a message to the LLM and yield responses
export async function* sendMessage(message) {
chatHistory.push({ role: "user", content: message });
// Get API configuration (custom or default)
const apiConfig = await getAPIConfig();
let apiUrl, headers, model;
if (apiConfig.isCustom) {
// Use custom API configuration
apiUrl = `${apiConfig.endpoint}/chat/completions`;
headers = apiConfig.headers;
model = apiConfig.model;
} else {
// Use default Hermes configuration
apiUrl = `${getSelectedModelEndpoint()}/chat/completions`;
headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
};
model = getSelectedModel();
}
const response = await fetch(apiUrl, {
method: "POST",
headers: headers,
body: JSON.stringify({
model: model,
messages: chatHistory,
temperature: 0.5,
max_tokens: 8192,
stream: true,
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i].trim();
if (line.startsWith("data: ")) {
const jsonData = line.slice(6);
if (jsonData === "[DONE]") continue;
try {
const parsedData = JSON.parse(jsonData);
const content = parsedData.choices[0].delta.content;
if (content) {
yield content;
}
} catch (error) {
console.error("Error parsing JSON:", error);
}
}
}
buffer = lines[lines.length - 1];
}
}
// Function to handle user input and display responses
export async function handleUserInput() {
const userInput = document.getElementById("user-input").value;
document.getElementById("user-input").value = "";
const chatBox = document.getElementById("chat-box");
chatBox.innerHTML += `<p><strong>You:</strong> ${userInput}</p>`;
const aiResponseParagraph = document.createElement("p");
aiResponseParagraph.innerHTML = "<strong>AI:</strong> ";
chatBox.appendChild(aiResponseParagraph);
const responseContent = document.createElement("span");
aiResponseParagraph.appendChild(responseContent);
let accumulatedContent = "";
for await (const chunk of sendMessage(userInput)) {
accumulatedContent += chunk;
const parsedChunk = marked.parse(accumulatedContent);
responseContent.innerHTML = parsedChunk;
responseContent.querySelectorAll("pre code").forEach((block) => {
hljs.highlightElement(block);
});
}
// Streaming is complete - add copy buttons to code blocks
addCodeBlockCopyButtons(responseContent);
chatBox.scrollTop = chatBox.scrollHeight;
// Add the response to chat history
chatHistory.push({ role: "assistant", content: accumulatedContent });
saveConversationHistory(chatHistory);
// Add play/pause button for TTS
const playPauseButton = document.createElement("button");
playPauseButton.textContent = "Generate TTS for AI Response";
playPauseButton.className = "uncloseai-ui-button-margin";
let aiAudio = null;
let aiBlob = null;
let isPaused = false;
playPauseButton.onclick = async () => {
if (!aiAudio) {
playPauseButton.textContent = "Processing...";
playPauseButton.disabled = true; // Disable button while processing
const mainVoiceSelect = document.getElementById("read-page-voice");
const selectedVoice = mainVoiceSelect ? mainVoiceSelect.value : "alloy";
const result = await speakText(accumulatedContent, selectedVoice, 0.9);
aiAudio = result.audio;
aiBlob = result.blob;
playPauseButton.textContent = "Pause AI Response";
playPauseButton.disabled = false; // Re-enable button after processing
aiAudio.play();
// Generate title for the MP3 file
const title = await generateTitleForTTS(accumulatedContent);
// Add download button
const downloadButton = document.createElement("button");
downloadButton.textContent = "Download MP3";
downloadButton.className = "uncloseai-ui-button-margin";
downloadButton.onclick = () => {
const a = document.createElement("a");
a.href = URL.createObjectURL(aiBlob);
a.download = `${title}.mp3`;
a.click();
};
chatBox.appendChild(downloadButton);
} else {
if (isPaused) {
aiAudio.play();
playPauseButton.textContent = "Pause AI Response";
} else {
aiAudio.pause();
playPauseButton.textContent = "Play AI Response";
}
isPaused = !isPaused;
}
};
chatBox.appendChild(playPauseButton);
}
// Update chat history reference (for external modules)
export function updateChatHistory(newHistory) {
chatHistory = newHistory;
}
export function getChatHistory() {
return chatHistory;
}
// Generator function to send a message with custom history (doesn't modify global chatHistory)
export async function* sendMessageWithCustomHistory(customHistory) {
// Get API configuration (custom or default)
const apiConfig = await getAPIConfig();
let apiUrl, headers, model;
if (apiConfig.isCustom) {
// Use custom API configuration
apiUrl = `${apiConfig.endpoint}/chat/completions`;
headers = apiConfig.headers;
model = apiConfig.model;
} else {
// Use default Hermes configuration
apiUrl = `${getSelectedModelEndpoint()}/chat/completions`;
headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
};
model = getSelectedModel();
}
const response = await fetch(apiUrl, {
method: "POST",
headers: headers,
body: JSON.stringify({
model: model,
messages: customHistory,
stream: true,
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error("Chat API Error:", {
status: response.status,
url: apiUrl,
response: errorText
});
throw new Error(`HTTP error! status: ${response.status}: ${errorText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") {
return;
}
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) {
yield content;
}
} catch (error) {
// Skip malformed JSON
}
}
}
}
} finally {
reader.releaseLock();
}
}