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.
291 lines
10 KiB
291 lines
10 KiB
<?php
|
|
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
|
|
require_once __DIR__ . '/default.php';
|
|
require_once __DIR__ . '/auth.php';
|
|
|
|
if (!isLoggedIn() && isGuestModeEnabled()) {
|
|
loginAsGuest();
|
|
}
|
|
|
|
if (!isLoggedIn()) {
|
|
header('Location: login.php');
|
|
exit;
|
|
}
|
|
|
|
$authUser = getAuthenticatedUser();
|
|
$isGuestSession = isGuestSession();
|
|
?>
|
|
<!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">
|
|
<span style="font-size:12px; color:#aaa;">Signed in as <?php echo htmlspecialchars(($authUser['username'] ?? 'guest')); ?></span>
|
|
<?php if (($authUser['role'] ?? 'user') === 'admin'): ?>
|
|
<a href="useradmin.php" style="color:#89b4fa; text-decoration:none;">User Admin</a>
|
|
<?php endif; ?>
|
|
<?php if (isGuestModeEnabled() && isGuestSession()): ?>
|
|
<a href="login.php" style="color:#89b4fa; text-decoration:none;">Login</a>
|
|
<?php endif; ?>
|
|
<a href="logout.php" style="color:#f38ba8; text-decoration:none;">Logout</a>
|
|
<select id="model-select">
|
|
<option>Loading models...</option>
|
|
</select>
|
|
<button class="danger" 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 });
|
|
|
|
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;
|
|
}
|
|
|
|
async function loadConfig() {
|
|
try {
|
|
const response = await fetch('savechat.php?action=load-config');
|
|
const data = await response.json();
|
|
return data.defaultModel || '';
|
|
} catch (err) {
|
|
console.error(err);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
async function saveConfig(defaultModel) {
|
|
try {
|
|
await fetch('savechat.php?action=save-config', {
|
|
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>';
|
|
}
|
|
}
|
|
|
|
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');
|
|
historyDateDiv.textContent = chat.created || '';
|
|
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.onclick = () => loadChat(chat.id);
|
|
const deleteBtn = document.createElement('button');
|
|
deleteBtn.textContent = 'Delete';
|
|
deleteBtn.onclick = async (event) => {
|
|
event.stopPropagation();
|
|
historyList.removeChild(item);
|
|
await fetch('savechat.php?action=delete&id=' + encodeURIComponent(chat.id));
|
|
loadHistory();
|
|
};
|
|
item.appendChild(deleteBtn);
|
|
historyList.appendChild(item);
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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.';
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
sendBtn.addEventListener('click', sendMessage);
|
|
userInput.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessage();
|
|
}
|
|
});
|
|
|
|
loadModels();
|
|
loadHistory();
|
|
</script>
|
|
</body>
|
|
</html>
|