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…
Reference in new issue