Files
KrisHD_AI/public/script.js
T

729 lines
24 KiB
JavaScript

const authDiv = document.getElementById("auth");
const chatDiv = document.getElementById("chat");
const authMsg = document.getElementById("authMsg");
const messagesDiv = document.getElementById("messages");
const loginForm = document.getElementById("loginForm");
const registerForm = document.getElementById("registerForm");
const loginUsernameInput = document.getElementById("loginUsername");
const loginPasswordInput = document.getElementById("loginPassword");
const registerUsernameInput = document.getElementById("registerUsername");
const registerPasswordInput = document.getElementById("registerPassword");
const loginSubmit = document.getElementById("loginSubmit");
const registerSubmit = document.getElementById("registerSubmit");
const msgInput = document.getElementById("msgInput");
const sendBtn = document.getElementById("send");
const authScreens = document.querySelectorAll(".auth-screen");
const toggleButtons = document.querySelectorAll(".auth-toggle .toggle");
const chatList = document.getElementById("chatList");
const newChatBtn = document.getElementById("newChat");
const renameChatBtn = document.getElementById("renameChat");
const deleteChatBtn = document.getElementById("deleteChat");
const activeChatTitle = document.getElementById("activeChatTitle");
const personaList = document.getElementById("personaList");
const userPersonaList = document.getElementById("userPersonaList");
const personaForm = document.getElementById("personaForm");
const personaFormTitle = document.getElementById("personaFormTitle");
const personaTypeSelect = document.getElementById("personaType");
const personaNameInput = document.getElementById("personaName");
const personaPronounsInput = document.getElementById("personaPronouns");
const personaAppearanceInput = document.getElementById("personaAppearance");
const personaBackgroundInput = document.getElementById("personaBackground");
const personaDetailsInput = document.getElementById("personaDetails");
const newPersonaBtn = document.getElementById("newPersona");
const newUserPersonaBtn = document.getElementById("newUserPersona");
const clearPersonaBtn = document.getElementById("clearPersona");
const clearUserPersonaBtn = document.getElementById("clearUserPersona");
const personaCancelBtn = document.getElementById("personaCancel");
const activePersonaStatus = document.getElementById("activePersonaStatus");
const activeUserPersonaStatus = document.getElementById("activeUserPersonaStatus");
const personaModal = document.getElementById("personaModal");
const personaCloseBtn = document.getElementById("personaClose");
const personaMenuButton = document.getElementById("personaMenuButton");
const personaPopover = document.getElementById("personaPopover");
let isProcessing = false;
let chatSessions = [];
let activeChatId = null;
let assistantPersonas = [];
let userPersonas = [];
let activePersonaId = null;
let activeUserPersonaId = null;
let editingPersonaId = null;
let editingPersonaType = "assistant";
let publishedPersonaIds = new Set();
const displayModels = async () => {
const select = document.getElementById("model");
fetch("/models")
.then(response => response.json())
.then(data => {
select.innerHTML = "";
data.models.forEach(model => {
const option = document.createElement("option");
option.value = model.model;
option.textContent = model.name;
select.appendChild(option);
});
})
.catch(err => {
console.error("Failed to load models:", err);
select.innerHTML = `<option>Error loading models</option>`;
});
}
async function request(url, data, method = "POST") {
try {
const res = await fetch(url, {
method,
headers: {"Content-Type": "application/json"},
body: data ? JSON.stringify(data) : undefined
});
return await res.json();
} catch (err) {
return {error: "Network error — please try again."};
}
}
async function post(url, data) {
return request(url, data, "POST");
}
async function get(url) {
try {
const res = await fetch(url);
return await res.json();
} catch (err) {
return {error: "Network error — please try again."};
}
}
function setAuthMessage(msg, state = "") {
authMsg.textContent = msg;
authMsg.className = state ? `status ${state}` : 'status';
}
function setMessageContent(element, content) {
if (window.marked && window.DOMPurify) {
const rendered = marked.parse(content, {breaks: true});
element.innerHTML = DOMPurify.sanitize(rendered);
return;
}
element.textContent = content;
}
function addMessage(content, isUser = false, isLoading = false, saveToHistory = true) {
const msgDiv = document.createElement('div');
msgDiv.className = `msg ${isUser ? 'user' : 'bot'}${isLoading ? ' loading' : ''}`;
if (isLoading) {
msgDiv.innerHTML = `
<div class="typing-indicator">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
`;
} else {
const header = document.createElement('div');
header.className = 'msg-header';
header.textContent = isUser ? 'You' : 'DeepFake';
const body = document.createElement('div');
body.className = 'msg-content';
setMessageContent(body, content);
msgDiv.appendChild(header);
msgDiv.appendChild(body);
}
messagesDiv.appendChild(msgDiv);
setTimeout(() => {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}, 100);
return msgDiv;
}
function clearMessages() {
messagesDiv.innerHTML = '';
}
function getChatById(chatId) {
return chatSessions.find(chat => chat.id === chatId);
}
function bumpChat(chatId) {
const index = chatSessions.findIndex(chat => chat.id === chatId);
if (index <= 0) return;
const [chat] = chatSessions.splice(index, 1);
chatSessions.unshift(chat);
renderChatList();
}
function updateChatActionState() {
const hasChat = Boolean(activeChatId);
renameChatBtn.disabled = !hasChat;
deleteChatBtn.disabled = !hasChat;
}
function renderChatList() {
chatList.innerHTML = '';
if (!chatSessions.length) {
const empty = document.createElement('p');
empty.className = 'status';
empty.textContent = 'No chats yet. Start a new one!';
chatList.appendChild(empty);
return;
}
chatSessions.forEach(chat => {
const item = document.createElement('div');
item.className = `chat-item${chat.id === activeChatId ? ' active' : ''}`;
const title = document.createElement('span');
title.textContent = chat.title;
const actions = document.createElement('button');
actions.type = 'button';
actions.textContent = '✕';
actions.title = 'Delete chat';
actions.addEventListener('click', (event) => {
event.stopPropagation();
deleteChat(chat.id);
});
item.appendChild(title);
item.appendChild(actions);
item.addEventListener('click', () => setActiveChat(chat.id));
chatList.appendChild(item);
});
}
function updateActivePersonaStatus(personaItems, activeId, statusElement, emptyMessage, activePrefix) {
const activePersona = personaItems.find(persona => persona.id === activeId);
if (activePersona) {
statusElement.textContent = `${activePrefix}: ${activePersona.name}`;
} else {
statusElement.textContent = emptyMessage;
}
}
function setActivePersonaStatus() {
updateActivePersonaStatus(
assistantPersonas,
activePersonaId,
activePersonaStatus,
'No AI persona equipped.',
'AI persona equipped'
);
updateActivePersonaStatus(
userPersonas,
activeUserPersonaId,
activeUserPersonaStatus,
'No user persona set.',
'User persona active'
);
clearPersonaBtn.disabled = !activePersonaId;
clearUserPersonaBtn.disabled = !activeUserPersonaId;
}
function openPersonaForm(persona = null, personaType = "assistant") {
closePersonaPopover();
personaModal.classList.remove("hidden");
if (persona) {
editingPersonaId = persona.id;
editingPersonaType = persona.persona_type || "assistant";
personaFormTitle.textContent = `Edit ${editingPersonaType === "user" ? "your persona" : "AI persona"}`;
personaTypeSelect.value = editingPersonaType;
personaTypeSelect.disabled = true;
personaNameInput.value = persona.name || "";
personaPronounsInput.value = persona.pronouns || "";
personaAppearanceInput.value = persona.appearance || "";
personaBackgroundInput.value = persona.background || "";
personaDetailsInput.value = persona.details || "";
} else {
editingPersonaId = null;
editingPersonaType = personaType;
personaFormTitle.textContent = `Create ${personaType === "user" ? "your persona" : "AI persona"}`;
personaTypeSelect.value = personaType;
personaTypeSelect.disabled = false;
personaNameInput.value = "";
personaPronounsInput.value = "";
personaAppearanceInput.value = "";
personaBackgroundInput.value = "";
personaDetailsInput.value = "";
}
personaNameInput.focus();
}
function closePersonaForm() {
personaModal.classList.add("hidden");
editingPersonaId = null;
editingPersonaType = "assistant";
personaTypeSelect.disabled = false;
}
function renderPersonaList(personaItems, activeId, listElement, personaType) {
listElement.innerHTML = '';
if (!personaItems.length) {
const empty = document.createElement('p');
empty.className = 'status persona-status';
empty.textContent = 'No personas yet. Create one to get started.';
listElement.appendChild(empty);
setActivePersonaStatus();
return;
}
personaItems.forEach(persona => {
const item = document.createElement('div');
item.className = `persona-item${persona.id === activeId ? ' active' : ''}`;
const titleRow = document.createElement('div');
titleRow.className = 'persona-title-row';
const title = document.createElement('h4');
title.textContent = persona.name;
const roleTag = document.createElement('span');
roleTag.className = `persona-role-tag ${personaType === "user" ? "user" : "ai"}`;
roleTag.textContent = personaType === "user" ? "You" : "AI";
titleRow.appendChild(title);
titleRow.appendChild(roleTag);
const meta = document.createElement('p');
meta.textContent = persona.pronouns ? `Pronouns: ${persona.pronouns}` : 'Pronouns: —';
const actions = document.createElement('div');
actions.className = 'persona-item-actions';
const equipBtn = document.createElement('button');
equipBtn.type = 'button';
const equipLabel = personaType === "user" ? "Equip as You" : "Equip as AI";
equipBtn.textContent = persona.id === activeId ? `${equipLabel} (Active)` : equipLabel;
equipBtn.disabled = persona.id === activeId;
equipBtn.addEventListener('click', () => equipPersona(persona.id, personaType));
const editBtn = document.createElement('button');
editBtn.type = 'button';
editBtn.textContent = 'Edit';
editBtn.addEventListener('click', () => openPersonaForm(persona));
const publishBtn = document.createElement('button');
publishBtn.type = 'button';
const isPublished = publishedPersonaIds.has(persona.id);
publishBtn.textContent = isPublished ? 'Update listing' : 'Publish';
publishBtn.addEventListener('click', () => publishPersona(persona.id));
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.textContent = 'Delete';
deleteBtn.addEventListener('click', () => deletePersona(persona.id));
actions.appendChild(equipBtn);
actions.appendChild(editBtn);
actions.appendChild(publishBtn);
actions.appendChild(deleteBtn);
item.appendChild(titleRow);
item.appendChild(meta);
item.appendChild(actions);
listElement.appendChild(item);
});
setActivePersonaStatus();
}
async function loadPersonas() {
const res = await get('/personas');
if (res.error) return;
assistantPersonas = res.assistantPersonas || [];
userPersonas = res.userPersonas || [];
activePersonaId = res.activePersonaId || null;
activeUserPersonaId = res.activeUserPersonaId || null;
publishedPersonaIds = new Set(res.publishedPersonaIds || []);
renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant");
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
}
async function equipPersona(personaId, personaType) {
const endpoint = personaType === "user"
? `/personas/${personaId}/equip-user`
: `/personas/${personaId}/equip`;
const res = await post(endpoint, {});
if (res.error) return;
if (personaType === "user") {
activeUserPersonaId = personaId;
} else {
activePersonaId = personaId;
}
renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant");
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
}
async function clearPersona(personaType) {
const endpoint = personaType === "user" ? '/personas/user/clear' : '/personas/clear';
const res = await post(endpoint, {});
if (res.error) return;
if (personaType === "user") {
activeUserPersonaId = null;
} else {
activePersonaId = null;
}
renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant");
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
}
async function savePersona() {
const payload = {
personaType: personaTypeSelect.value,
name: personaNameInput.value.trim(),
pronouns: personaPronounsInput.value.trim(),
appearance: personaAppearanceInput.value.trim(),
background: personaBackgroundInput.value.trim(),
details: personaDetailsInput.value.trim()
};
if (!payload.name) return;
let res;
if (editingPersonaId) {
res = await request(`/personas/${editingPersonaId}`, payload, "PUT");
} else {
res = await post('/personas', payload);
}
if (res.error) return;
await loadPersonas();
closePersonaForm();
}
async function deletePersona(personaId) {
const persona = assistantPersonas.find(item => item.id === personaId)
|| userPersonas.find(item => item.id === personaId);
if (!persona) return;
const confirmDelete = confirm(`Delete persona "${persona.name}"?`);
if (!confirmDelete) return;
const res = await request(`/personas/${personaId}`, null, "DELETE");
if (res.error) return;
await loadPersonas();
}
async function publishPersona(personaId) {
const res = await post(`/personas/${personaId}/publish`, {});
if (res.error) return;
await loadPersonas();
}
function setLoadingState(loading) {
isProcessing = loading;
sendBtn.disabled = loading;
msgInput.disabled = loading;
if (loading) {
sendBtn.classList.add('loading');
msgInput.placeholder = "Processing response...";
} else {
sendBtn.classList.remove('loading');
msgInput.placeholder = "Type your message...";
}
}
function showAuthScreen(target) {
authScreens.forEach(screen => {
screen.classList.toggle('active', screen.id === `${target}Screen`);
});
toggleButtons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.target === target);
});
setAuthMessage('', '');
}
function setActiveChat(chatId) {
activeChatId = chatId;
const chat = getChatById(chatId);
activeChatTitle.textContent = chat ? chat.title : 'New chat';
updateChatActionState();
renderChatList();
if (chatId) {
loadChatMessages(chatId);
} else {
clearMessages();
}
}
async function loadChatSessions() {
const res = await get('/chats');
if (res.error) return;
chatSessions = res.chats || [];
renderChatList();
const activeExists = chatSessions.find(chat => chat.id === activeChatId);
if (activeExists) {
setActiveChat(activeChatId);
return;
}
if (!chatSessions.length) {
await createNewChat();
return;
}
setActiveChat(chatSessions[0].id);
}
async function loadChatMessages(chatId) {
const res = await get(`/chats/${chatId}/messages`);
if (res.error) return;
clearMessages();
(res.messages || []).forEach(msg => {
addMessage(msg.content, msg.role === 'user', false, false);
});
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
async function createNewChat() {
const res = await post('/chats', {title: 'New chat'});
if (res.error || !res.chat) return;
chatSessions.unshift(res.chat);
setActiveChat(res.chat.id);
renderChatList();
}
async function renameChat(chatId) {
const chat = getChatById(chatId);
if (!chat) return;
const nextTitle = prompt('Rename chat', chat.title);
if (!nextTitle) return;
const trimmedTitle = nextTitle.trim();
if (!trimmedTitle) return;
const res = await request(`/chats/${chatId}`, {title: trimmedTitle}, "PUT");
if (res.error) return;
chat.title = trimmedTitle;
activeChatTitle.textContent = trimmedTitle;
renderChatList();
}
async function deleteChat(chatId) {
const chat = getChatById(chatId);
if (!chat) return;
const confirmDelete = confirm(`Delete "${chat.title}"? This cannot be undone.`);
if (!confirmDelete) return;
const res = await request(`/chats/${chatId}`, null, "DELETE");
if (res.error) return;
chatSessions = chatSessions.filter(item => item.id !== chatId);
if (activeChatId === chatId) {
if (chatSessions.length) {
setActiveChat(chatSessions[0].id);
} else {
activeChatId = null;
await createNewChat();
}
}
renderChatList();
}
async function checkSession() {
const res = await get('/session');
if (res.user) {
authDiv.style.display = "none";
chatDiv.style.display = "block";
await loadChatSessions();
await loadPersonas();
msgInput.focus();
return true;
}
return false;
}
async function loadServerChat() {
await loadChatSessions();
await loadPersonas();
}
async function handleAuth(endpoint, credentials) {
const {username, password} = credentials;
const submitBtn = endpoint === "login" ? loginSubmit : registerSubmit;
if (!username || !password) {
setAuthMessage("Please enter both username and password.", 'error');
return;
}
submitBtn.disabled = true;
setAuthMessage("Processing...", '');
const res = await post(`/${endpoint}`, {username, password});
submitBtn.disabled = false;
if (res.error) {
setAuthMessage(res.error, 'error');
} else {
if (endpoint === "login") {
setAuthMessage("Login successful! Redirecting to chat...", 'success');
authDiv.style.display = "none";
chatDiv.style.display = "block";
await loadServerChat();
msgInput.focus();
} else {
setAuthMessage("Registration successful — you can now log in!", 'success');
showAuthScreen('login');
loginUsernameInput.value = username;
loginPasswordInput.focus();
}
}
}
async function sendMessage() {
if (isProcessing) return;
const msg = msgInput.value.trim();
if (!msg) return;
if (!activeChatId) {
await createNewChat();
}
const currentChat = getChatById(activeChatId);
if (currentChat && currentChat.title === 'New chat') {
const autoTitle = msg.length > 40 ? `${msg.slice(0, 40)}…` : msg;
currentChat.title = autoTitle;
activeChatTitle.textContent = autoTitle;
renderChatList();
await request(`/chats/${activeChatId}`, {title: autoTitle}, "PUT");
}
addMessage(msg, true);
msgInput.value = "";
msgInput.style.height = 'auto';
setLoadingState(true);
const loadingMsg = addMessage('', false, true, false);
try {
const model = document.getElementById("model").value;
const res = await post("/chat", {message: msg, model, chatId: activeChatId});
if (messagesDiv.contains(loadingMsg)) {
messagesDiv.removeChild(loadingMsg);
}
if (res.reply) {
addMessage(res.reply, false);
bumpChat(activeChatId);
} else {
addMessage(`[Error: ${res.error}]`, false);
}
} catch (error) {
if (messagesDiv.contains(loadingMsg)) {
messagesDiv.removeChild(loadingMsg);
}
addMessage(`[Error: Network error]`, false);
} finally {
setLoadingState(false);
msgInput.focus();
}
}
toggleButtons.forEach(btn => {
btn.addEventListener('click', () => showAuthScreen(btn.dataset.target));
});
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
handleAuth("login", {
username: loginUsernameInput.value.trim(),
password: loginPasswordInput.value.trim()
});
});
registerForm.addEventListener('submit', (e) => {
e.preventDefault();
handleAuth("register", {
username: registerUsernameInput.value.trim(),
password: registerPasswordInput.value.trim()
});
});
document.getElementById("logout").onclick = async () => {
await post("/logout", {});
chatDiv.style.display = "none";
authDiv.style.display = "grid";
clearMessages();
chatSessions = [];
activeChatId = null;
assistantPersonas = [];
userPersonas = [];
activePersonaId = null;
activeUserPersonaId = null;
publishedPersonaIds = new Set();
setAuthMessage("Logged out.", 'success');
showAuthScreen('login');
loginUsernameInput.value = "";
loginPasswordInput.value = "";
};
sendBtn.onclick = sendMessage;
msgInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
newChatBtn.addEventListener('click', () => createNewChat());
renameChatBtn.addEventListener('click', () => {
if (activeChatId) {
renameChat(activeChatId);
}
});
deleteChatBtn.addEventListener('click', () => {
if (activeChatId) {
deleteChat(activeChatId);
}
});
msgInput.addEventListener('input', function () {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
newPersonaBtn.addEventListener('click', () => openPersonaForm());
newUserPersonaBtn.addEventListener('click', () => openPersonaForm(null, "user"));
clearPersonaBtn.addEventListener('click', () => clearPersona("assistant"));
clearUserPersonaBtn.addEventListener('click', () => clearPersona("user"));
personaForm.addEventListener('submit', (event) => {
event.preventDefault();
savePersona();
});
personaCloseBtn.addEventListener("click", closePersonaForm);
personaModal.addEventListener("click", (e) => {
if (e.target === personaModal) closePersonaForm();
});
function closePersonaPopover() {
personaPopover.classList.add("hidden");
personaMenuButton.setAttribute("aria-expanded", "false");
}
function togglePersonaPopover(event) {
event.stopPropagation();
const isHidden = personaPopover.classList.contains("hidden");
if (isHidden) {
personaPopover.classList.remove("hidden");
personaMenuButton.setAttribute("aria-expanded", "true");
} else {
closePersonaPopover();
}
}
personaMenuButton.addEventListener("click", togglePersonaPopover);
personaPopover.addEventListener("click", (event) => {
event.stopPropagation();
});
document.addEventListener("click", () => {
closePersonaPopover();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closePersonaPopover();
}
});
window.addEventListener('load', () => {
checkSession();
displayModels();
});