HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: /home/panomity.de/public_html/unhinged-ai/index.php
<?php
/**
 * ollama_chat_stream_dark_md.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"     → huihui_ai/gemma3-abliterated:12b
 *      - "Durchdacht"  → huihui_ai/qwen3.5-abliterated:35b
 *  - Assistant-Ausgabe: Markdown → HTML (clientseitig mit marked + DOMPurify, live beim Stream)
 *
 * Special requirement:
 *  - Auf die allererste User-Nachricht der Session:
 *      1) Diese Nachricht wird vor den Systemprompt gestellt (als Präfix) und
 *      2) zusätzlich als normale User-Nachricht gesendet.
 *
 * UI:
 *  - Minimal: keine Model-/Server-Badges, kein Admin-Bereich.
 */

declare(strict_types=1);
session_start();

// ---------------- Configuration ----------------
$OLLAMA_BASE        = getenv('UNHINGED_OLLAMA_BASE') ?: 'http://127.0.0.1:11434';
$FAST_MODEL         = 'huihui_ai/gemma3-abliterated:12b';
$THOUGHTFUL_MODEL   = 'huihui_ai/qwen3.5-abliterated:35b';

// 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_md.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();
  http_response_code(204);
  exit;
}

// ---------------- HTML (Dark Chat UI + Markdown→HTML) ----------------
$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>
  <!-- Markdown parser & sanitizer -->
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></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; }
    .msg { white-space: pre-wrap; }

    /* Markdown Styling (dark) */
    .md blockquote { border-left: 3px solid #475569; color: #cbd5e1; padding-left: .75rem; margin: .5rem 0; }
    .md h1, .md h2, .md h3, .md h4 { color: #e5e7eb; margin-top: .75rem; margin-bottom: .25rem; font-weight: 700; }
    .md h1 { font-size: 1.25rem; }
    .md h2 { font-size: 1.125rem; }
    .md h3 { font-size: 1rem; }
    .md p { margin: .25rem 0 .5rem; }
    .md a { color: #93c5fd; text-decoration: underline; }
    .md ul, .md ol { padding-left: 1.25rem; margin: .25rem 0 .5rem; }
    .md code { background: #0b1220; border: 1px solid #1f2937; padding: .125rem .25rem; border-radius: .375rem; }
    .md pre { background: #0b1220; border: 1px solid #1f2937; padding: .75rem; border-radius: .75rem; overflow: auto; }
    .md pre code { background: transparent; border: none; padding: 0; }
    .md table { width: 100%; border-collapse: collapse; margin: .5rem 0; }
    .md th, .md td { border: 1px solid #1f2937; padding: .375rem .5rem; }
    .md th { background: #0f172a; color: #e5e7eb; }
  </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. Du benötigst viel Geduld im durchdachten Modus.
          </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">
                <!-- Wir geben hier den Rohtext aus; JS rendert ihn später als HTML -->
                <div class="assistant-md max-w-[85%] rounded-2xl rounded-bl-sm bg-slate-800 text-slate-100 px-4 py-2 shadow msg md">
                  <?= 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 12a 8 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>
  (() => {
    // --- marked setup ---
    // Schönere Defaults für Chat:
    marked.setOptions({
      gfm: true,
      breaks: true,
      headerIds: false,
      mangle: false
    });

    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';
    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; // User: niemals HTML
      wrap.appendChild(bubble);
      chat.appendChild(wrap);
      chat.scrollTop = chat.scrollHeight;
    }

    // Erstellt eine Assistenten-Blase, die Markdown → HTML rendert
    function appendAssistantBubble() {
      const wrap = document.createElement('div');
      wrap.className = 'flex justify-start';
      const bubble = document.createElement('div');
      bubble.className = 'assistant-md max-w-[85%] rounded-2xl rounded-bl-sm bg-slate-800 text-slate-100 px-4 py-2 shadow md';
      bubble.textContent = ''; // Start leer; wir streamen gleich hinein
      // Rohpuffer pro Blase
      bubble.__raw = '';
      wrap.appendChild(bubble);
      chat.appendChild(wrap);
      chat.scrollTop = chat.scrollHeight;
      return bubble;
    }

    // Render-Funktion: Markdown → (sanitized) HTML
    function renderMarkdownTo(el, raw) {
      try {
        const html = marked.parse(raw);
        const clean = DOMPurify.sanitize(html, {USE_PROFILES: {html: true}});
        el.innerHTML = clean;
        // Links sicher öffnen
        for (const a of el.querySelectorAll('a')) {
          a.setAttribute('target', '_blank');
          a.setAttribute('rel', 'nofollow noopener noreferrer');
        }
      } catch (e) {
        // Fallback: zeige Rohtext
        el.textContent = raw;
      }
      chat.scrollTop = chat.scrollHeight;
    }

    // Bereits vorhandene Assistenten-Antworten (aus dem Verlauf) nachträglich rendern
    function renderExistingAssistantMarkdown() {
      document.querySelectorAll('.assistant-md').forEach(el => {
        const raw = el.textContent || '';
        el.__raw = raw;
        renderMarkdownTo(el, raw);
      });
    }
    renderExistingAssistantMarkdown();

    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;
          const chunk = decoder.decode(value, { stream: true });
          // Rohtext anfügen und sofort neu rendern
          assistantBubble.__raw += chunk;
          renderMarkdownTo(assistantBubble, assistantBubble.__raw);
        }
      } catch (err) {
        if (err.name !== 'AbortError') {
          // Auch Fehlermeldungen in Markdown rendern
          assistantBubble.__raw += '\n\n**[Netzwerkfehler]** ' + String(err);
          renderMarkdownTo(assistantBubble, assistantBubble.__raw);
        }
      } finally {
        setBusy(false);
        controller = null;
      }
    });

    // UX: Enter = senden, Shift+Enter = Zeilenumbruch
    promptEl.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        sendBtn.click();
      }
    });
  })();
  </script>
</body>
</html>