You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

380 lines
11 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="default.css">
<title>KaI (Ollama Remote Chat)</title>
<script src="marked.umd.js"></script>
</head>
<body>
<div class="layout">
<div class="sidebar">
<div class="sidebar-header">
Chat History
</div>
<div id="history-list">
</div>
</div>
<div class="chat-container">
<div class="chat-header">
<h2>I'm KaI your - Ollama Chat</h2>
<div class="controls">
<select id="model-select">
<option>Loading models...</option>
</select>
<button
class="new-chat-btn"
id="new-chat-btn">
New Chat
</button>
</div>
</div>
<div class="chat-messages" id="chat-messages">
<div class="message system">Conversation started.</div>
</div>
<div class="chat-input-area">
<textarea id="user-input" placeholder="Type your message..."></textarea>
<button class="send-btn" id="send-btn">Send</button>
</div>
</div>
</div>
<script>
const chatMessages = document.getElementById('chat-messages');
const historyList = document.getElementById('history-list');
const userInput = document.getElementById('user-input');
const sendBtn = document.getElementById('send-btn');
const newChatBtn = document.getElementById('new-chat-btn');
const modelSelect = document.getElementById('model-select');
let chatHistory = [];
let currentChatId = null;
marked.setOptions({
breaks:true
});
/* ==========================================================
Message Output
========================================================== */
function appendMessage(role, text) {
const msgDiv = document.createElement("div");
msgDiv.classList.add("message", role);
if (role === "assistant") {
msgDiv.innerHTML = marked.parse(text);
} else {
msgDiv.textContent = text;
}
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return msgDiv;
}
/* ==========================================================
Models
========================================================== */
async function loadConfig() {
try {
const response = await fetch("config.php?action=load");
const data = await response.json();
return data.defaultModel || "";
} catch (err) {
console.error(err);
return "";
}
}
async function saveConfig(defaultModel) {
try {
await fetch("config.php?action=save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ defaultModel })
});
} catch (err) {
console.error(err);
}
}
async function loadModels() {
try {
const response = await fetch("chat.php?action=models");
const data = await response.json();
const defaultModel = await loadConfig();
modelSelect.innerHTML = "";
if (data.models) {
let selectedModel = "";
data.models.forEach(model => {
const option = document.createElement("option");
option.value = model.name;
option.textContent = model.name;
modelSelect.appendChild(option);
if (!selectedModel && model.name === defaultModel) {
selectedModel = model.name;
}
});
const fallbackModel = selectedModel || (data.models[0] && data.models[0].name) || "";
if (fallbackModel) {
modelSelect.value = fallbackModel;
} else {
modelSelect.innerHTML = "<option>No Models</option>";
}
} else {
modelSelect.innerHTML = "<option>No Models</option>";
}
}
catch {
modelSelect.innerHTML = "<option>Connection failed</option>";
}
}
/* ==========================================================
History
========================================================== */
async function loadHistory() {
try {
const response = await fetch("savechat.php?action=list");
const chats = await response.json();
historyList.innerHTML = "";
chats.forEach(chat => {
const item = document.createElement("div");
item.className = "history-item";
if (chat.id === currentChatId) item.classList.add("active");
item.innerHTML = `<div class="history-title">${chat.title}</div>`;
const historyDateDiv = document.createElement("div");
const historyDate = chat.created || '';
historyDateDiv.textContent = historyDate;
historyDateDiv.className = "history-date";
item.appendChild(historyDateDiv);
const historyModelSpan = document.createElement("span");
historyModelSpan.textContent = `${chat.model}`;
historyModelSpan.style.display = "inline-block";
historyModelSpan.style.marginLeft = "10px";
historyModelSpan.style.float = "right";
historyModelSpan.className = "history-date";
item.appendChild(historyModelSpan);
item.appendChild(historyDateDiv);
item.onclick = () => {
loadChat(chat.id);
};
// add a delete button here
const deleteBtn = document.createElement("button");
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = async () => {
// remove the chat from the list and from the database
historyList.removeChild(item);
await fetch('savechat.php?action=delete&id=' + encodeURIComponent(chat.id));
loadHistory(); // reload the history to update the list
};
item.appendChild(deleteBtn);
historyList.appendChild(item);
});
} catch (err) {
console.error(err);
}
}
/* ==========================================================
Load Chat
========================================================== */
async function loadChat(id) {
try {
const response =
await fetch(
"savechat.php?action=load&id="
+ encodeURIComponent(id)
);
const chat = await response.json();
if (chat.error) {
alert(chat.error);
return;
}
currentChatId = chat.id;
chatHistory = chat.messages;
chatMessages.innerHTML = "";
chat.messages.forEach(msg => {
appendMessage(
msg.role,
msg.content
);
});
loadHistory();
}
catch(err) {
console.error(err);
}
}
/* ==========================================================
Save Chat
========================================================== */
async function saveChat() {
if (chatHistory.length === 0) return;
try {
const response = await fetch( "savechat.php?action=save", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({id: currentChatId, model: modelSelect.value, messages: chatHistory})});
const result = await response.json();
if (result.id) currentChatId = result.id;
loadHistory();
}
catch(err) {
console.error(err);
}
}
/* ==========================================================
Send Message
========================================================== */
async function sendMessage() {
const text = userInput.value.trim();
if (!text) return;
if (!modelSelect.value) {
alert("Please select a model.");
return;
}
appendMessage("user", text);
chatHistory.push({role: "user", content: text});
userInput.value = "";
const thinking =
appendMessage( "assistant",'<div class="thinking-text">Thinking<span>.</span><span>.</span><span>.</span></div>');
thinking.classList.add("thinking-state");
const startTime = performance.now();
try {
const response =
await fetch("chat.php", {
method: "POST",
headers: { "Content-Type":"application/json" },
body: JSON.stringify({ model: modelSelect.value, messages: chatHistory})
});
thinking.classList.remove("thinking-state");
const data = await response.json();
const endTime = performance.now();
const totalDuration = ((endTime-startTime)/1000).toFixed(2);
if (data.error) {
thinking.textContent = data.error;
return;
}
const html = marked.parse(data.message.content);
const tokens = data.eval_count || 0;
const promptTokens = data.prompt_eval_count || 0;
const generationSeconds = data.eval_duration ? data.eval_duration / 1000000000 : 0;
const tps = (tokens>0 && generationSeconds>0) ? (tokens/generationSeconds).toFixed(1) : 0;
let footer = "Time: " + totalDuration + " sec";
if(tokens>0){
footer += " | Prompt " + promptTokens + " t | Answer " + tokens
+ " t (" + tps + " t/s)";
}
thinking.innerHTML =
html +
'<div style="margin-top:8px;font-size:11px;color:#999;text-align:right;font-style:italic;">'
+ footer +
'</div>';
chatHistory.push({
role:"assistant",
content:data.message.content
});
await saveChat();
}
catch(err){
console.error(err);
thinking.classList.remove("thinking-state");
thinking.textContent =
"Backend connection failed.";
}
}
/* ==========================================================
New Chat
========================================================== */
newChatBtn.addEventListener("click", () => {
currentChatId = null;
chatHistory = [];
chatMessages.innerHTML = "";
appendMessage("system","Conversation started.");
loadHistory();
});
modelSelect.addEventListener("change", () => {
if (modelSelect.value && modelSelect.value !== "Loading models..." && modelSelect.value !== "No Models" && modelSelect.value !== "Connection failed") {
saveConfig(modelSelect.value);
}
});
/* ==========================================================
Keyboard
========================================================== */
sendBtn.addEventListener("click",sendMessage);
userInput.addEventListener("keydown", function(e){
if(e.key==="Enter" && !e.shiftKey){
e.preventDefault();
sendMessage();
}
});
/* ==========================================================
Startup
========================================================== */
loadModels();
loadHistory();
</script>
</body>
</html>