parent
03bf8450e2
commit
e59c5cae4b
@ -0,0 +1,247 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureConfigDirectory() {
|
||||||
|
if (!is_dir(CONFIG_PATH)) {
|
||||||
|
if (!mkdir(CONFIG_PATH, 0755, true) && !is_dir(CONFIG_PATH)) {
|
||||||
|
throw new RuntimeException('Unable to create config directory.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfigData() {
|
||||||
|
ensureConfigDirectory();
|
||||||
|
|
||||||
|
if (!file_exists(CONFIG_FILE)) {
|
||||||
|
return ['defaultModel' => '', 'users' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = @file_get_contents(CONFIG_FILE);
|
||||||
|
if ($content === false) {
|
||||||
|
return ['defaultModel' => '', 'users' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($content, true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
return ['defaultModel' => '', 'users' => []];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($data['users']) || !is_array($data['users'])) {
|
||||||
|
$data['users'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfigData($config) {
|
||||||
|
ensureConfigDirectory();
|
||||||
|
|
||||||
|
$data = getConfigData();
|
||||||
|
if (array_key_exists('defaultModel', $config)) {
|
||||||
|
$data['defaultModel'] = (string) $config['defaultModel'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('users', $config)) {
|
||||||
|
$data['users'] = is_array($config['users']) ? $config['users'] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
return file_put_contents(CONFIG_FILE, $json) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readConfig() {
|
||||||
|
$data = getConfigData();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'defaultModel' => isset($data['defaultModel']) ? (string) $data['defaultModel'] : '',
|
||||||
|
'users' => isset($data['users']) && is_array($data['users']) ? $data['users'] : []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeConfig($config) {
|
||||||
|
return saveConfigData($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashPassword($password) {
|
||||||
|
return password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyPassword($password, $hash) {
|
||||||
|
return password_verify($password, $hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadUsers() {
|
||||||
|
$config = readConfig();
|
||||||
|
return $config['users'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUsers($users) {
|
||||||
|
return writeConfig(['users' => $users]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findUserByUsername($username) {
|
||||||
|
$username = trim((string) $username);
|
||||||
|
if ($username === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (loadUsers() as $user) {
|
||||||
|
if (($user['username'] ?? '') === $username) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findUserByEmail($email) {
|
||||||
|
$email = trim((string) $email);
|
||||||
|
if ($email === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (loadUsers() as $user) {
|
||||||
|
if (strtolower((string) ($user['email'] ?? '')) === strtolower($email)) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUser($username, $password, $role = 'user', $email = '') {
|
||||||
|
$username = trim((string) $username);
|
||||||
|
$email = trim((string) $email);
|
||||||
|
if ($username === '' || $password === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = loadUsers();
|
||||||
|
if (findUserByUsername($username) !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email !== '' && findUserByEmail($email) !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$users[] = [
|
||||||
|
'username' => $username,
|
||||||
|
'email' => $email,
|
||||||
|
'passwordHash' => hashPassword($password),
|
||||||
|
'role' => $role
|
||||||
|
];
|
||||||
|
|
||||||
|
return saveUsers($users);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestPasswordReset($email) {
|
||||||
|
$email = trim((string) $email);
|
||||||
|
if ($email === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = findUserByEmail($email);
|
||||||
|
if ($user === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$users = loadUsers();
|
||||||
|
foreach ($users as &$entry) {
|
||||||
|
if (($entry['username'] ?? '') === ($user['username'] ?? '')) {
|
||||||
|
$entry['resetTokenHash'] = password_hash($token, PASSWORD_DEFAULT);
|
||||||
|
$entry['resetTokenExpires'] = time() + 3600;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!saveUsers($users)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$link = APP_BASE_URL . '/reset.php?token=' . urlencode($token);
|
||||||
|
$subject = 'Password reset for KaI';
|
||||||
|
$message = "Hello {$user['username']},\n\n"
|
||||||
|
. "You requested a password reset for your KaI account.\n"
|
||||||
|
. "Please use the following link to set a new password:\n\n"
|
||||||
|
. $link . "\n\n"
|
||||||
|
. "If you did not request this, you can ignore this email.";
|
||||||
|
$headers = "From: " . MAIL_FROM_NAME . " <" . MAIL_FROM . ">\r\n"
|
||||||
|
. "Reply-To: " . MAIL_FROM . "\r\n"
|
||||||
|
. "X-Mailer: PHP/" . phpversion();
|
||||||
|
|
||||||
|
return mail($email, $subject, $message, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePasswordResetToken($token) {
|
||||||
|
$token = (string) $token;
|
||||||
|
if ($token === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (loadUsers() as $user) {
|
||||||
|
$expiresAt = (int) ($user['resetTokenExpires'] ?? 0);
|
||||||
|
if ($expiresAt > time() && !empty($user['resetTokenHash']) && password_verify($token, (string) $user['resetTokenHash'])) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function completePasswordReset($token, $newPassword) {
|
||||||
|
$token = (string) $token;
|
||||||
|
$newPassword = (string) $newPassword;
|
||||||
|
if ($token === '' || $newPassword === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = loadUsers();
|
||||||
|
foreach ($users as &$entry) {
|
||||||
|
$expiresAt = (int) ($entry['resetTokenExpires'] ?? 0);
|
||||||
|
if ($expiresAt > time() && !empty($entry['resetTokenHash']) && password_verify($token, (string) $entry['resetTokenHash'])) {
|
||||||
|
$entry['passwordHash'] = hashPassword($newPassword);
|
||||||
|
unset($entry['resetTokenHash'], $entry['resetTokenExpires']);
|
||||||
|
return saveUsers($users);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLoggedIn() {
|
||||||
|
return !empty($_SESSION['auth_user']['username']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthenticatedUser() {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $_SESSION['auth_user'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAuthentication() {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Authentication required.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAdmin() {
|
||||||
|
$user = getAuthenticatedUser();
|
||||||
|
if (!$user || (($user['role'] ?? 'user') !== 'admin')) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Admin access required.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logoutUser() {
|
||||||
|
unset($_SESSION['auth_user']);
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
define('OLLAMA_URL', 'http://127.0.0.1:11434');
|
||||||
|
define('CONFIG_PATH', '/var/lib/KaI');
|
||||||
define('CONFIG_PATH', '/home/www/data/kai-ollama');
|
|
||||||
define('CONFIG_FILE', CONFIG_PATH . '/config.json');
|
define('CONFIG_FILE', CONFIG_PATH . '/config.json');
|
||||||
define('CHAT_HISTORY_FILE', CONFIG_PATH . '/chat.history.data');
|
define('CHAT_HISTORY_FILE', CONFIG_PATH . '/chat.history.data');
|
||||||
|
define('MAIL_FROM', getenv('MAIL_FROM') ?: 'noreply@kai.local');
|
||||||
|
define('MAIL_FROM_NAME', getenv('MAIL_FROM_NAME') ?: 'KaI');
|
||||||
|
define('APP_BASE_URL', getenv('APP_BASE_URL') ?: 'http://127.0.0.1/KaI-OllamaChat');
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/default.php';
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
|
||||||
|
$message = '';
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$email = trim((string) ($_POST['email'] ?? ''));
|
||||||
|
if ($email === '') {
|
||||||
|
$errors[] = 'Please enter your email address.';
|
||||||
|
} elseif (requestPasswordReset($email)) {
|
||||||
|
$message = 'If the email exists in our records, a reset link has been sent.';
|
||||||
|
} else {
|
||||||
|
$message = 'If the email exists in our records, a reset link has been sent.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Forgot Password - KaI</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; background: #1e1e2e; color: #cdd6f4; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
||||||
|
.card { background: #11111b; padding: 24px; border-radius: 10px; width: 360px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
|
||||||
|
input, button { width: 100%; padding: 10px; border-radius: 6px; border: 1px solid #45475a; background: #313244; color: #fff; margin-top: 10px; }
|
||||||
|
button { background: #89b4fa; color: #11111b; font-weight: bold; cursor: pointer; }
|
||||||
|
.message { color: #a6e3a1; margin-top: 12px; }
|
||||||
|
.error { color: #f38ba8; margin-top: 12px; }
|
||||||
|
a { color: #89b4fa; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Reset password</h1>
|
||||||
|
<p>Enter your email address and we will send you a reset link.</p>
|
||||||
|
<form method="post">
|
||||||
|
<input type="email" name="email" required placeholder="you@example.com">
|
||||||
|
<button type="submit">Send reset link</button>
|
||||||
|
</form>
|
||||||
|
<?php if (!empty($errors)): ?><div class="error"><?php echo htmlspecialchars(implode('<br>', $errors)); ?></div><?php endif; ?>
|
||||||
|
<?php if ($message !== ''): ?><div class="message"><?php echo htmlspecialchars($message); ?></div><?php endif; ?>
|
||||||
|
<p><a href="login.php">Back to login</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,282 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/default.php';
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$authUser = getAuthenticatedUser();
|
||||||
|
?>
|
||||||
|
<!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'] ?? ''); ?></span>
|
||||||
|
<?php if (($authUser['role'] ?? 'user') === 'admin'): ?>
|
||||||
|
<a href="useradmin.php" style="color:#89b4fa; text-decoration:none;">User Admin</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('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>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/default.php';
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
$message = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$password = (string) ($_POST['password'] ?? '');
|
||||||
|
|
||||||
|
if ($username === '' || $password === '') {
|
||||||
|
$errors[] = 'Please enter both username and password.';
|
||||||
|
} else {
|
||||||
|
$user = findUserByUsername($username);
|
||||||
|
if ($user && verifyPassword($password, $user['passwordHash'] ?? '')) {
|
||||||
|
$_SESSION['auth_user'] = [
|
||||||
|
'username' => $user['username'],
|
||||||
|
'role' => $user['role'] ?? 'user'
|
||||||
|
];
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors[] = 'Invalid username or password.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoggedIn()) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists(CONFIG_FILE)) {
|
||||||
|
ensureConfigDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty(loadUsers())) {
|
||||||
|
$created = createUser('admin', 'changeme', 'admin', 'admin@example.com');
|
||||||
|
if ($created) {
|
||||||
|
$message = 'Default admin account created. Username: admin / Password: changeme';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - KaI</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; background: #1e1e2e; color: #cdd6f4; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
||||||
|
.card { background: #11111b; padding: 24px; border-radius: 10px; width: 360px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
|
||||||
|
h1 { margin-top: 0; font-size: 1.4rem; }
|
||||||
|
label { display: block; margin-top: 12px; font-weight: bold; }
|
||||||
|
input { width: 100%; padding: 10px; margin-top: 6px; border-radius: 6px; border: 1px solid #45475a; background: #313244; color: #fff; }
|
||||||
|
button { margin-top: 16px; width: 100%; padding: 10px; border: none; border-radius: 6px; background: #89b4fa; color: #11111b; font-weight: bold; cursor: pointer; }
|
||||||
|
.error { color: #f38ba8; margin-top: 12px; }
|
||||||
|
.message { color: #a6e3a1; margin-top: 12px; }
|
||||||
|
.small { font-size: 0.9rem; color: #999; margin-top: 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>KaI Login</h1>
|
||||||
|
<form method="post">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input id="username" name="username" required>
|
||||||
|
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input id="password" name="password" type="password" required>
|
||||||
|
|
||||||
|
<button type="submit">Sign in</button>
|
||||||
|
</form>
|
||||||
|
<?php if (!empty($errors)): ?>
|
||||||
|
<div class="error">
|
||||||
|
<?php echo htmlspecialchars(implode('<br>', $errors)); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($message !== ''): ?>
|
||||||
|
<div class="message"><?php echo htmlspecialchars($message); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="small">Default admin account is created on first run: admin / changeme</div>
|
||||||
|
<div class="small"><a href="forgot.php" style="color:#89b4fa;">Forgot password?</a></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
|
||||||
|
logoutUser();
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/default.php';
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
|
||||||
|
$message = '';
|
||||||
|
$errors = [];
|
||||||
|
$token = (string) ($_GET['token'] ?? '');
|
||||||
|
|
||||||
|
if ($token === '') {
|
||||||
|
$errors[] = 'Missing reset token.';
|
||||||
|
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$token = (string) ($_POST['token'] ?? '');
|
||||||
|
$newPassword = (string) ($_POST['password'] ?? '');
|
||||||
|
if ($token === '' || $newPassword === '') {
|
||||||
|
$errors[] = 'Please provide a new password.';
|
||||||
|
} elseif (completePasswordReset($token, $newPassword)) {
|
||||||
|
$message = 'Password updated successfully. You can now log in.';
|
||||||
|
} else {
|
||||||
|
$errors[] = 'The reset link is invalid or expired.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Reset Password - KaI</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; background: #1e1e2e; color: #cdd6f4; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
||||||
|
.card { background: #11111b; padding: 24px; border-radius: 10px; width: 360px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
|
||||||
|
input, button { width: 100%; padding: 10px; border-radius: 6px; border: 1px solid #45475a; background: #313244; color: #fff; margin-top: 10px; }
|
||||||
|
button { background: #89b4fa; color: #11111b; font-weight: bold; cursor: pointer; }
|
||||||
|
.message { color: #a6e3a1; margin-top: 12px; }
|
||||||
|
.error { color: #f38ba8; margin-top: 12px; }
|
||||||
|
a { color: #89b4fa; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Create a new password</h1>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token); ?>">
|
||||||
|
<input type="password" name="password" required placeholder="New password">
|
||||||
|
<button type="submit">Save new password</button>
|
||||||
|
</form>
|
||||||
|
<?php if (!empty($errors)): ?><div class="error"><?php echo htmlspecialchars(implode('<br>', $errors)); ?></div><?php endif; ?>
|
||||||
|
<?php if ($message !== ''): ?><div class="message"><?php echo htmlspecialchars($message); ?></div><?php endif; ?>
|
||||||
|
<p><a href="login.php">Back to login</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/default.php';
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
|
||||||
|
requireAuthentication();
|
||||||
|
requireAdmin();
|
||||||
|
|
||||||
|
$users = loadUsers();
|
||||||
|
$errors = [];
|
||||||
|
$success = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'create') {
|
||||||
|
$username = trim((string) ($_POST['username'] ?? ''));
|
||||||
|
$password = (string) ($_POST['password'] ?? '');
|
||||||
|
$email = trim((string) ($_POST['email'] ?? ''));
|
||||||
|
$role = ($_POST['role'] ?? 'user') === 'admin' ? 'admin' : 'user';
|
||||||
|
|
||||||
|
if ($username === '' || $password === '') {
|
||||||
|
$errors[] = 'Username and password are required.';
|
||||||
|
} elseif (!createUser($username, $password, $role, $email)) {
|
||||||
|
$errors[] = 'User already exists or could not be created.';
|
||||||
|
} else {
|
||||||
|
$users = loadUsers();
|
||||||
|
$success = 'User created successfully.';
|
||||||
|
}
|
||||||
|
} elseif ($action === 'update') {
|
||||||
|
$index = (int) ($_POST['index'] ?? -1);
|
||||||
|
$username = trim((string) ($_POST['username'] ?? ''));
|
||||||
|
$email = trim((string) ($_POST['email'] ?? ''));
|
||||||
|
$password = (string) ($_POST['password'] ?? '');
|
||||||
|
$role = ($_POST['role'] ?? 'user') === 'admin' ? 'admin' : 'user';
|
||||||
|
|
||||||
|
if ($index >= 0 && isset($users[$index])) {
|
||||||
|
if ($username !== '') {
|
||||||
|
$users[$index]['username'] = $username;
|
||||||
|
}
|
||||||
|
if ($email !== '') {
|
||||||
|
$users[$index]['email'] = $email;
|
||||||
|
}
|
||||||
|
if ($password !== '') {
|
||||||
|
$users[$index]['passwordHash'] = hashPassword($password);
|
||||||
|
}
|
||||||
|
$users[$index]['role'] = $role;
|
||||||
|
if (saveUsers($users)) {
|
||||||
|
$success = 'User updated successfully.';
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Unable to update user.';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Invalid user selected.';
|
||||||
|
}
|
||||||
|
} elseif ($action === 'delete') {
|
||||||
|
$index = (int) ($_POST['index'] ?? -1);
|
||||||
|
if ($index >= 0 && isset($users[$index])) {
|
||||||
|
unset($users[$index]);
|
||||||
|
$users = array_values($users);
|
||||||
|
if (saveUsers($users)) {
|
||||||
|
$success = 'User deleted successfully.';
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Unable to delete user.';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Invalid user selected.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$users = loadUsers();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>User Management - KaI</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="default.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="topbar">
|
||||||
|
<h1>User Management</h1>
|
||||||
|
<a href="index.php">Back to chat</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($success !== ''): ?><div class="success"><?php echo htmlspecialchars($success); ?></div><?php endif; ?>
|
||||||
|
<?php if (!empty($errors)): ?><div class="error"><?php echo htmlspecialchars(implode('<br>', $errors)); ?></div><?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Create user</h2>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="create">
|
||||||
|
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:10px;">
|
||||||
|
<div><label>Username</label><br><input name="username" required></div>
|
||||||
|
<div><label>Email</label><br><input name="email" type="email"></div>
|
||||||
|
<div><label>Password</label><br><input name="password" type="password" required></div>
|
||||||
|
<div><label>Role</label><br><select name="role"><option value="user">User</option><option value="admin">Admin</option></select></div>
|
||||||
|
<div><label> </label><button type="submit">Create</button></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Existing users</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Username</th><th>Email</th><th>Role</th><th>Actions</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($users as $index => $user): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<form method="post" style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
|
||||||
|
<input type="hidden" name="action" value="update">
|
||||||
|
<input type="hidden" name="index" value="<?php echo (int) $index; ?>">
|
||||||
|
<input name="username" value="<?php echo htmlspecialchars($user['username'] ?? ''); ?>" style="width: 150px"; required>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input name="email" type="email" value="<?php echo htmlspecialchars($user['email'] ?? ''); ?>">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select name="role">
|
||||||
|
<option value="user" <?php if (($user['role'] ?? 'user') === 'user') echo 'selected'; ?>>User</option>
|
||||||
|
<option value="admin" <?php if (($user['role'] ?? 'user') === 'admin') echo 'selected'; ?>>Admin</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input name="password" type="password" placeholder="New password" style="width: 150px">
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" style="display:inline-block; margin-left:8px;">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="index" value="<?php echo (int) $index; ?>">
|
||||||
|
<button class="danger" type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in new issue