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.

308 lines
11 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 })
});
let result = null;
try {
result = await response.json();
} catch (e) {
console.error('Failed to parse save response', e);
const text = await response.text();
console.error('Raw save response:', text);
throw new Error('Save failed: invalid server response');
}
console.debug('Save response:', result);
if (result && result.error) {
console.error('Save error:', result.error);
alert('Chat save failed: ' + result.error);
throw new Error(result.error);
}
if (result && result.id) currentChatId = result.id;
await loadHistory();
return result;
} 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>