File: //home/panomity.de/public_html/unhinged-ai/index1.php
<?php
/**
* ollama_chat_stream_dark.php
*
* Single-file PHP app for:
* - Dark-mode chat UI (Verdana)
* - Streaming multi-turn *chat* responses from Ollama (/api/chat)
* - System prompt loaded from ./system.txt
* - Mode switch in UI:
* - "Schnell" → gpt-oss:20b
* - "Durchdacht" → gpt-oss:120b
*
* Special requirement:
* - On the very first user message of a session, prepend that message to the
* system prompt (as a prefix), and ALSO send it as the normal user message.
*
* UI specifics:
* - No model/server/system badges
* - No admin/system prompt editor
*/
declare(strict_types=1);
session_start();
// ---------------- Configuration ----------------
$OLLAMA_BASE = getenv('UNHINGED_OLLAMA_BASE') ?: 'http://127.0.0.1:11434';
$FAST_MODEL = 'gpt-oss:20b';
$THOUGHTFUL_MODEL = 'gpt-oss:120b';
// Optional generation settings for Ollama
$OPTIONS = [
'temperature' => 0.7,
// 'num_ctx' => 4096,
// 'top_p' => 0.9,
// 'repeat_penalty' => 1.05,
];
$UNHINGED_CONFIG = [
'ollama_base' => $OLLAMA_BASE,
'gpuq_api_base' => 'http://127.0.0.1:8020',
'execute_url' => 'https://panomity.de/unhinged-ai/' . basename(__FILE__) . '?execute=1',
'fast_model' => $FAST_MODEL,
'thoughtful_model' => $THOUGHTFUL_MODEL,
'options' => $OPTIONS,
];
require_once __DIR__ . '/chat-backend.php';
// ---------------- Routing ----------------
if (isset($_GET['execute'])) {
unhinged_handle_execute_request($UNHINGED_CONFIG);
}
if (PHP_SAPI === 'cli') {
// CLI usage: php ollama_chat_stream_dark.php "your prompt" [fast|deep]
$prompt = $argv[1] ?? '';
$mode = $argv[2] ?? 'fast';
if ($prompt === '') {
fwrite(STDERR, "Usage: php " . basename(__FILE__) . " \"your prompt\" [fast|deep]\n");
exit(1);
}
unhinged_stream_chat($prompt, $mode === 'deep' ? 'deep' : 'fast', $UNHINGED_CONFIG);
exit;
}
if (isset($_GET['stream'])) {
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
$prompt = '';
$mode = 'fast';
if (is_array($data)) {
if (isset($data['prompt'])) $prompt = (string)$data['prompt'];
if (isset($data['mode'])) $mode = (string)$data['mode'];
} else {
$prompt = $_POST['q'] ?? $_GET['q'] ?? '';
$mode = $_POST['mode'] ?? $_GET['mode'] ?? 'fast';
}
$mode = ($mode === 'deep') ? 'deep' : 'fast';
if (trim($prompt) === '') {
http_response_code(400);
header('Content-Type: text/plain; charset=utf-8');
echo "Missing prompt.";
exit;
}
unhinged_stream_chat($prompt, $mode, $UNHINGED_CONFIG);
exit;
}
if (isset($_GET['reset'])) {
reset_session_chat();
// Respond with 204 No Content for fetch-based reset
http_response_code(204);
exit;
}
// ---------------- HTML (Dark Chat UI) ----------------
$history = current_history();
?>
<!doctype html>
<html lang="de" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="dark" />
<meta name="theme-color" content="#0b1020" />
<title>Chat</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { font-family: Verdana, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; }
#chatScroll::-webkit-scrollbar { width: 8px; }
#chatScroll::-webkit-scrollbar-thumb { background: #4f46e5; border-radius: 6px; }
#chatScroll::-webkit-scrollbar-track { background: #111827; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
.msg { white-space: pre-wrap; }
</style>
</head>
<body class="min-h-screen bg-gradient-to-br from-slate-950 via-indigo-950 to-slate-900 text-slate-200">
<div class="max-w-4xl mx-auto p-4 sm:p-6">
<div class="rounded-2xl shadow-xl bg-slate-900 ring-1 ring-slate-800 flex flex-col h-[80vh]">
<!-- Header -->
<div class="px-4 py-3 border-b border-slate-800">
<div class="flex items-center justify-between gap-3">
<h1 class="text-lg md:text-xl font-bold tracking-tight text-white">Chat</h1>
<div class="flex items-center gap-3">
<label for="modeSelect" class="text-xs text-slate-400">Modus</label>
<select id="modeSelect" class="text-xs rounded-lg border border-slate-700 bg-slate-800 text-slate-200 px-3 py-1.5 hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-indigo-600">
<option value="fast" selected>Schnell</option>
<option value="deep">Durchdacht</option>
</select>
<button id="resetBtn"
class="text-xs rounded-lg border border-slate-700 bg-slate-800 px-3 py-1.5 font-semibold text-slate-200 hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-indigo-600">
Neue Unterhaltung
</button>
</div>
</div>
</div>
<!-- Chat history -->
<div id="chatScroll" class="flex-1 overflow-auto p-4 space-y-4">
<?php if (empty($history)): ?>
<div class="text-sm text-slate-400">
Starte eine Unterhaltung unten. Im durchdachten Modus benötigst du Geduld.
</div>
<?php else: ?>
<?php foreach ($history as $m): ?>
<?php if (($m['role'] ?? '') === 'user'): ?>
<div class="flex justify-end">
<div class="max-w-[85%] rounded-2xl rounded-br-sm bg-indigo-600 text-white px-4 py-2 shadow msg"><?= htmlspecialchars((string)$m['content'], ENT_QUOTES, 'UTF-8') ?></div>
</div>
<?php elseif (($m['role'] ?? '') === 'assistant'): ?>
<div class="flex justify-start">
<div class="max-w-[85%] rounded-2xl rounded-bl-sm bg-slate-800 text-slate-100 px-4 py-2 shadow msg"><?= htmlspecialchars((string)$m['content'], ENT_QUOTES, 'UTF-8') ?></div>
</div>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- Composer -->
<form id="chatForm" class="border-t border-slate-800 p-3">
<div class="flex items-end gap-2">
<textarea id="prompt" name="q" rows="2"
class="flex-1 resize-y min-h-[2.5rem] max-h-48 rounded-xl border border-slate-700 bg-slate-900 text-slate-100 placeholder-slate-500 p-3 shadow-sm outline-none focus:ring-2 focus:ring-indigo-600 focus:border-indigo-600"
placeholder="Schreibe eine Nachricht…"></textarea>
<button id="sendBtn" type="submit"
class="inline-flex items-center gap-2 rounded-xl bg-indigo-600 text-white px-4 py-2 font-semibold shadow hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 disabled:opacity-60">
<svg id="spinner" class="animate-spin h-4 w-4 hidden" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
</svg>
<span id="btnLabel">Senden</span>
</button>
<button id="stopBtn" type="button"
class="inline-flex items-center gap-2 rounded-xl bg-slate-700 text-slate-200 px-4 py-2 font-semibold shadow hover:bg-slate-600 focus:ring-2 focus:ring-slate-500 disabled:opacity-60"
disabled>Stopp</button>
</div>
<p class="mt-1 text-xs text-slate-500">
Tipp: Mit <kbd class="px-1 py-0.5 bg-slate-800 border border-slate-700 rounded">Shift</kbd> + <kbd class="px-1 py-0.5 bg-slate-800 border border-slate-700 rounded">Enter</kbd> fügst du einen Zeilenumbruch ein.
</p>
</form>
</div>
</div>
<script>
(() => {
const form = document.getElementById('chatForm');
const promptEl = document.getElementById('prompt');
const chat = document.getElementById('chatScroll');
const sendBtn = document.getElementById('sendBtn');
const stopBtn = document.getElementById('stopBtn');
const spinner = document.getElementById('spinner');
const btnLabel = document.getElementById('btnLabel');
const resetBtn = document.getElementById('resetBtn');
const modeSel = document.getElementById('modeSelect');
const MODE_KEY = 'ollama.chat.mode';
// Restore saved mode
const savedMode = localStorage.getItem(MODE_KEY);
if (savedMode === 'fast' || savedMode === 'deep') {
modeSel.value = savedMode;
}
modeSel.addEventListener('change', () => {
localStorage.setItem(MODE_KEY, modeSel.value);
});
let controller = null;
function setBusy(busy) {
sendBtn.disabled = busy;
stopBtn.disabled = !busy;
spinner.classList.toggle('hidden', !busy);
btnLabel.textContent = busy ? 'Streaming…' : 'Senden';
}
function appendUserBubble(text) {
const wrap = document.createElement('div');
wrap.className = 'flex justify-end';
const bubble = document.createElement('div');
bubble.className = 'max-w-[85%] rounded-2xl rounded-br-sm bg-indigo-600 text-white px-4 py-2 shadow msg';
bubble.textContent = text;
wrap.appendChild(bubble);
chat.appendChild(wrap);
chat.scrollTop = chat.scrollHeight;
}
function appendAssistantBubble() {
const wrap = document.createElement('div');
wrap.className = 'flex justify-start';
const bubble = document.createElement('div');
bubble.className = 'max-w-[85%] rounded-2xl rounded-bl-sm bg-slate-800 text-slate-100 px-4 py-2 shadow msg';
bubble.textContent = '';
wrap.appendChild(bubble);
chat.appendChild(wrap);
chat.scrollTop = chat.scrollHeight;
return bubble;
}
stopBtn.addEventListener('click', () => {
if (controller) controller.abort();
setBusy(false);
});
resetBtn.addEventListener('click', async () => {
try {
await fetch(window.location.pathname + '?reset=1', { method: 'POST' });
} catch (e) {}
window.location.reload();
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
const prompt = promptEl.value.trim();
if (!prompt) {
promptEl.focus();
return;
}
appendUserBubble(prompt);
promptEl.value = '';
const assistantBubble = appendAssistantBubble();
controller = new AbortController();
setBusy(true);
try {
const resp = await fetch(window.location.pathname + '?stream=1', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, mode: modeSel.value }),
signal: controller.signal
});
if (!resp.ok || !resp.body) {
const text = await resp.text().catch(()=>'');
assistantBubble.textContent = 'Fehler: ' + (text || (resp.status + ' ' + resp.statusText));
setBusy(false);
return;
}
const reader = resp.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
assistantBubble.textContent += decoder.decode(value, { stream: true });
chat.scrollTop = chat.scrollHeight;
}
} catch (err) {
if (err.name !== 'AbortError') {
assistantBubble.textContent += '\n[Netzwerkfehler] ' + err;
}
} finally {
setBusy(false);
controller = null;
}
});
// UX: Enter to send, Shift+Enter for newline
promptEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendBtn.click();
}
});
})();
</script>
</body>
</html>