main
BuildTools 4 weeks ago
commit 03bf8450e2

@ -0,0 +1,6 @@
## Information
you can use this code at your own risk.
## markup.umd.js
This file is downloaded from [https://github.com/markedjs/marked](https://github.com/markedjs/marked) which is used unter its MIT license.

@ -0,0 +1,10 @@
# KaI - Ollama Chat
a simple webpage which will connect to a remote ollama instance and allowes you to talk to any model aviable in ollama.
## configuration
all configuration is done in the file default.php

@ -0,0 +1,53 @@
<?php
// CHANGE THIS to your remote Ollama server IP or domain (e.g., http://192.168.1.50:11434)
define('OLLAMA_URL', 'http://10.2.0.3:11434');
header('Content-Type: application/json');
// 1. Handle fetching available models
if (isset($_GET['action']) && $_GET['action'] === 'models') {
$ch = curl_init(OLLAMA_URL . '/api/tags');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
if (curl_errno($ch)) {
echo json_encode(['error' => 'Could not connect to Ollama server.']);
} else {
echo $response;
}
curl_close($ch);
exit;
}
// 2. Handle sending chat messages
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$model = $input['model'] ?? 'llama3';
$messages = $input['messages'] ?? [];
$payload = json_encode([
'model' => $model,
'messages' => $messages,
'stream' => false
]);
$ch = curl_init(OLLAMA_URL . '/api/chat');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
$response = curl_exec($ch);
if (curl_errno($ch)) {
echo json_encode(['error' => 'Error communicating with remote Ollama: ' . curl_error($ch)]);
} else {
echo $response;
}
curl_close($ch);
exit;
}
echo json_encode(['error' => 'Invalid request']);

@ -0,0 +1,66 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/default.php';
function readConfig() {
if (!file_exists(CONFIG_FILE)) {
return ['defaultModel' => ''];
}
$content = file_get_contents(CONFIG_FILE);
if ($content === false) {
return ['defaultModel' => ''];
}
$data = json_decode($content, true);
if (!is_array($data)) {
return ['defaultModel' => ''];
}
return [
'defaultModel' => isset($data['defaultModel']) ? (string) $data['defaultModel'] : ''
];
}
function writeConfig($config) {
$data = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return file_put_contents(CONFIG_FILE, $data) !== false;
}
$action = $_GET['action'] ?? '';
switch ($action) {
case 'load':
echo json_encode(readConfig());
exit;
case 'save':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['error' => 'POST required.']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input)) {
echo json_encode(['error' => 'Invalid JSON.']);
exit;
}
$config = readConfig();
$config['defaultModel'] = isset($input['defaultModel']) ? (string) $input['defaultModel'] : '';
if (!writeConfig($config)) {
echo json_encode(['error' => 'Unable to save config.']);
exit;
}
echo json_encode(['success' => true, 'defaultModel' => $config['defaultModel']]);
exit;
default:
echo json_encode(['error' => 'Unknown action.']);
exit;
}

@ -0,0 +1,301 @@
* {
box-sizing: border-box;
margin:0;
padding:0;
font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;
}
body {
background:#1e1e2e;
color:#cdd6f4;
min-height:100vh;
overflow:hidden;
}
.layout {
display:flex;
height:100vh;
}
/* ==========================
LEFT SIDEBAR
========================== */
.sidebar {
width:280px;
background:#11111b;
border-right:1px solid #313244;
display:flex;
flex-direction:column;
}
.sidebar-header {
padding:15px;
font-size:18px;
font-weight:bold;
border-bottom:1px solid #313244;
}
#history-list {
flex:1;
overflow-y:auto;
}
.history-item {
padding:12px;
cursor:pointer;
border-bottom:1px solid #313244;
transition:background .2s;
}
.history-item:hover {
background:#313244;
}
.history-item.active {
background:#45475a;
}
.history-title {
font-size:14px;
font-weight:bold;
color:#cdd6f4;
margin-bottom:4px;
}
.history-date {
font-size:11px;
color:#888;
}
.chat-container {
flex:1;
display:flex;
flex-direction:column;
overflow:hidden;
}
.chat-header {
padding:15px 20px;
background:#11111b;
display:flex;
justify-content:space-between;
align-items:center;
border-bottom:1px solid #313244;
}
.controls {
display:flex;
gap:10px;
align-items:center;
}
select,
button {
background:#313244;
color:#cdd6f4;
border:1px solid #45475a;
padding:8px 12px;
border-radius:6px;
cursor:pointer;
}
button.new-chat-btn {
background:#f38ba8;
color:#11111b;
font-weight:bold;
border:none;
}
button.send-btn {
background:#89b4fa;
color:#11111b;
font-weight:bold;
border:none;
}
.chat-messages {
flex:1;
overflow-y:auto;
padding:20px;
display:flex;
flex-direction:column;
gap:15px;
}
.message {
max-width:90%;
padding:12px 16px;
border-radius:8px;
word-break:break-word;
line-height:1.5;
white-space: pre-wrap;
}
.message.user {
align-self:flex-end;
background:#89b4fa;
color:#11111b;
}
.message.system {
align-self:center;
color:#999;
background:transparent;
font-style:italic;
}
.message pre {
background:#11111b;
padding:10px;
overflow:auto;
border-radius:5px;
}
.message code {
background:#11111b;
padding:2px 5px;
border-radius:3px;
}
.message ul,
.message ol {
margin-left:20px;
}
.chat-input-area {
display:flex;
gap:10px;
padding:15px;
background:#11111b;
border-top:1px solid #313244;
}
textarea {
flex:1;
resize:none;
height:60px;
background:#313244;
color:white;
border:1px solid #45475a;
border-radius:6px;
padding:12px;
}
textarea:focus {
outline:none;
border-color:#89b4fa;
}
@property --border-angle {
syntax:'<angle>';
inherits:false;
initial-value:0deg;
}
@keyframes rotate-border {
to {
--border-angle:360deg;
}
}
.message.assistant {
align-self: flex-start;
background-color: #313244;
color: #cdd6f4;
border-bottom-left-radius: 2px;
border: 3px solid transparent; /* Platzhalter für den Rahmen */
}
.message.assistant.thinking-state {
background-image: linear-gradient(#313244, #313244),
conic-gradient(from var(--border-angle), #00ffff, #000000, #ff00ff, #000000, #ffffff, #000000, #00ffff);
background-origin: border-box;
background-clip: padding-box, border-box;
animation: rotate-border 1s linear infinite; /* Von 3s auf 1s verkürzt für mehr Speed */
}
.thinking-text span {
animation:blink 1.4s infinite both;
}
.thinking-text span:nth-child(2){
animation-delay:.2s;
}
.thinking-text span:nth-child(3){
animation-delay:.4s;
}
@keyframes blink{
0%{opacity:.2;}
20%{opacity:1;}
100%{opacity:.2;}
}
@media screen and (max-width: 900px) {
body {
overflow:auto;
}
.layout {
flex-direction:column;
height:auto;
min-height:100vh;
}
.sidebar {
width:100%;
max-height:320px;
border-right:none;
border-bottom:1px solid #313244;
}
.chat-container {
height:auto;
}
.chat-header {
flex-wrap:wrap;
gap:10px;
}
.controls {
width:100%;
justify-content:space-between;
flex-wrap:wrap;
}
.chat-messages {
padding:16px;
min-height:280px;
}
.chat-input-area {
flex-direction:column;
gap:10px;
}
textarea {
height:100px;
}
button.send-btn,
button.new-chat-btn,
select {
width:100%;
}
}
/* Monospace font for inline code and pre blocks */
code,
pre,
kbd,
samp,
tt,
.message code,
.message pre {
font-family: Consolas, "Andale Mono", "Lucida Console", Monaco, "Courier New", monospace;
}

@ -0,0 +1,8 @@
<?php
header('Content-Type: application/json');
define('CONFIG_PATH', '/home/www/data/kai-ollama');
define('CONFIG_FILE', CONFIG_PATH . '/config.json');
define('CHAT_HISTORY_FILE', CONFIG_PATH . '/chat.history.data');

@ -0,0 +1,379 @@
<!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>

File diff suppressed because one or more lines are too long

@ -0,0 +1,241 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/default.php';
/*----------------------------------------------------------
- Create storage file if necessary
----------------------------------------------------------*/
if (!file_exists(CHAT_HISTORY_FILE)) {
if (@file_put_contents(CHAT_HISTORY_FILE, '') === false) {
http_response_code(500);
echo json_encode([
'error' => 'Cannot create history file.'
]);
exit;
}
}
/*----------------------------------------------------------
- Helper
----------------------------------------------------------*/
function readChats() {
$result = [];
$fp = fopen(CHAT_HISTORY_FILE, 'c+');
if (!$fp) {
return [];
}
flock($fp, LOCK_SH);
while (($line = fgets($fp)) !== false) {
$line = trim($line);
if ($line === '') {
continue;
}
$obj = json_decode($line, true);
if (is_array($obj)) {
$result[] = $obj;
}
}
flock($fp, LOCK_UN);
fclose($fp);
return $result;
}
function writeChats($chats) {
$fp = fopen(CHAT_HISTORY_FILE, 'w');
if (!$fp) {
return false;
}
flock($fp, LOCK_EX);
foreach ($chats as $chat) {
fwrite($fp, json_encode($chat, JSON_UNESCAPED_UNICODE) . PHP_EOL);
}
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
return true;
}
function createTitle($messages) {
foreach ($messages as $msg) {
if (($msg['role'] ?? '') === 'user') {
$title = trim($msg['content']);
if (mb_strlen($title) > 60) {
$title = mb_substr($title, 0, 60) . "...";
}
return $title;
}
}
return "Untitled Chat";
}
/*----------------------------------------------------------
- Action
----------------------------------------------------------*/
$action = $_GET['action'] ?? '';
switch ($action) {
/*------------------------------------------------------
- LIST
------------------------------------------------------*/
case 'list':
$chats = readChats();
$list = [];
foreach ($chats as $chat) {
$list[] = [
'id' => $chat['id'],
'title' => $chat['title'],
'created' => $chat['created'],
'model' => $chat['model'] ?? ''
];
}
usort($list, function($a, $b) {
return strcmp($b['created'], $a['created']);
});
echo json_encode($list);
exit;
/*------------------------------------------------------
- LOAD
------------------------------------------------------*/
case 'load':
$id = $_GET['id'] ?? '';
if ($id == '') {
echo json_encode([
'error' => 'Missing id.'
]);
exit;
}
$chats = readChats();
foreach ($chats as $chat) {
if ($chat['id'] === $id) {
echo json_encode($chat);
exit;
}
}
echo json_encode([
'error' => 'Chat not found.'
]);
exit;
/*------------------------------------------------------
- SAVE
------------------------------------------------------*/
case 'save':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode([
'error' => 'POST required.'
]);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input)) {
echo json_encode([
'error' => 'Invalid JSON.'
]);
exit;
}
$messages = $input['messages'] ?? [];
if (!is_array($messages) || count($messages) == 0) {
echo json_encode([
'error' => 'No messages.'
]);
exit;
}
$id = $input['id'] ?? '';
if ($id == '') {
$id = uniqid('', true);
}
$title = createTitle($messages);
$model = $input['model'] ?? '';
$chat = [
'id' => $id,
'title' => $title,
'created' => date('Y-m-d H:i:s'),
'model' => $model,
'messages' => $messages
];
$chats = readChats();
$found = false;
foreach ($chats as &$existing) {
if ($existing['id'] === $id) {
$existing = $chat;
$found = true;
break;
}
}
if (!$found) {
$chats[] = $chat;
}
if (!writeChats($chats)) {
echo json_encode([
'error' => 'Unable to save.'
]);
exit;
}
echo json_encode([
'success' => true,
'id' => $id,
'title' => $title
]);
exit;
/*------------------------------------------------------
- DELETE
------------------------------------------------------*/
case 'delete':
$id = $_GET['id'] ?? '';
if ($id == '') {
echo json_encode([
'error' => 'Missing id.'
]);
exit;
}
$chats = readChats();
$newChats = [];
foreach ($chats as $chat) {
if ($chat['id'] !== $id) {
$newChats[] = $chat;
}
}
writeChats($newChats);
echo json_encode([
'success' => true
]);
exit;
/*------------------------------------------------------
- DEFAULT
------------------------------------------------------*/
default:
echo json_encode([
'error' => 'Unknown action.'
]);
exit;
}
Loading…
Cancel
Save