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
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>
|
|
|