Backup #90

ID90
Dateipfad/var/www/dev.campus.systemische-tools.de/src/View/chat/index.php
Version8
Typ modified
Größe21.4 KB
Hashabf077e1862cf78da5f2cc8212d629e0b2f565039d4a240b178113fa78bc013b
Datum2025-12-20 19:16:31
Geändert vonclaude-code-hook
GrundClaude Code Pre-Hook Backup vor Edit-Operation
Datei existiert Ja

Dateiinhalt

<?php ob_start(); ?>

<div class="chat-layout">
    <!-- Sidebar -->
    <aside class="chat-sidebar" id="chat-sidebar">
        <div class="chat-sidebar__header">
            <a href="/chat" class="btn btn--primary btn--full">+ Neuer Chat</a>
        </div>
        <div class="chat-sidebar__sessions" id="session-list">
            <?php foreach ($sessions ?? [] as $s):
                $totalTokens = (int) ($s['total_input_tokens'] ?? 0) + (int) ($s['total_output_tokens'] ?? 0);
                $totalCost = ((int) ($s['total_input_tokens'] ?? 0) * 0.000015) + ((int) ($s['total_output_tokens'] ?? 0) * 0.000075);
                ?>
            <a href="/chat/<?= $s['uuid'] ?>"
               class="session-item <?= ($session['uuid'] ?? '') === $s['uuid'] ? 'session-item--active' : '' ?>"
               data-uuid="<?= $s['uuid'] ?>">
                <div class="session-item__title" id="title-<?= $s['uuid'] ?>"><?= htmlspecialchars($s['title'] ?? 'Neuer Chat') ?></div>
                <div class="session-item__meta">
                    <?php $isOllama = str_starts_with($s['model'] ?? '', 'ollama:'); ?>
                    <span class="session-item__model"><?= $isOllama ? 'Ollama' : 'Claude' ?></span>
                    <span class="session-item__count"><?= $s['message_count'] ?? 0 ?> Nachr.</span>
                    <?php if (!$isOllama && $totalTokens > 0): ?>
                    <span class="session-item__tokens" title="<?= number_format((int) $s['total_input_tokens']) ?> in / <?= number_format((int) $s['total_output_tokens']) ?> out">
                        <?= number_format($totalTokens) ?> Tok.
                    </span>
                    <span class="session-item__cost" title="Geschätzte Kosten">~$<?= number_format($totalCost, 2) ?></span>
                    <?php elseif ($isOllama): ?>
                    <span class="session-item__tokens local">lokal</span>
                    <?php endif; ?>
                </div>
                <div class="session-item__actions">
                    <button class="session-item__edit"
                            onclick="event.preventDefault(); event.stopPropagation(); editSessionTitle('<?= $s['uuid'] ?>');"
                            title="Titel bearbeiten">
                        &#9998;
                    </button>
                    <button class="session-item__delete"
                            hx-delete="/chat/<?= $s['uuid'] ?>"
                            hx-confirm="Session wirklich löschen?"
                            onclick="event.preventDefault(); event.stopPropagation();">
                        &times;
                    </button>
                </div>
            </a>
            <?php endforeach; ?>
            <?php if (empty($sessions)): ?>
            <div class="session-item session-item--empty">Keine Sessions</div>
            <?php endif; ?>
        </div>
    </aside>

    <!-- Toggle Button for Mobile -->
    <button class="chat-sidebar-toggle" id="sidebar-toggle" aria-label="Sidebar umschalten">
        <span></span><span></span><span></span>
    </button>

    <!-- Main Chat Area -->
    <main class="chat-main">
        <div class="chat-container">
            <div class="chat-header">
                <h1><?= htmlspecialchars($session['title'] ?? 'KI-Chat') ?></h1>
                <p>Fragen zu systemischem Teamcoaching & Teamentwicklung</p>
            </div>

            <div class="chat-messages" id="chat-messages">
                <!-- Welcome message if no messages -->
                <?php if (empty($messages)): ?>
                <div class="chat-message chat-message--assistant">
                    <div class="message-content">
                        Hallo! Ich bin dein Assistent für Fragen zu systemischem Teamcoaching.
                        Stelle mir eine Frage und ich durchsuche die verfügbaren Dokumente.
                    </div>
                </div>
                <?php endif; ?>

                <!-- Existing messages -->
                <?php foreach ($messages ?? [] as $msg): ?>
                <div class="chat-message chat-message--<?= $msg['role'] ?>">
                    <div class="message-content">
                        <?php if ($msg['role'] === 'user'): ?>
                            <?= htmlspecialchars($msg['content']) ?>
                        <?php else: ?>
                            <?= nl2br(htmlspecialchars($msg['content'])) ?>
                            <?php if (!empty($msg['sources'])): ?>
                                <?php $sources = json_decode($msg['sources'], true) ?: []; ?>
                                <?php if (!empty($sources)): ?>
                                <?php $uniqueId = 'sources-' . $msg['id']; ?>
                                <div class="chat-sources chat-sources--collapsed" id="<?= $uniqueId ?>">
                                    <button type="button" class="chat-sources__toggle" onclick="toggleSources('<?= $uniqueId ?>')">
                                        <span class="chat-sources__count"><?= count($sources) ?> Quelle<?= count($sources) > 1 ? 'n' : '' ?></span>
                                        <span class="chat-sources__arrow">&#9660;</span>
                                    </button>
                                    <div class="chat-sources__list">
                                    <?php foreach ($sources as $source): ?>
                                        <div class="source-item">
                                            <div class="source-item__header">
                                                <span class="source-item__title"><?= htmlspecialchars($source['title'] ?? 'Unbekannt') ?></span>
                                                <span class="source-item__score"><?= round(($source['score'] ?? 0) * 100) ?>%</span>
                                            </div>
                                            <?php if (!empty($source['content'])): ?>
                                            <div class="source-item__content">"<?= htmlspecialchars(mb_substr($source['content'], 0, 200)) ?><?= mb_strlen($source['content']) > 200 ? '...' : '' ?>"</div>
                                            <?php endif; ?>
                                        </div>
                                    <?php endforeach; ?>
                                    </div>
                                </div>
                                <?php endif; ?>
                            <?php endif; ?>
                            <?php
                                $inputTokens = (int) ($msg['tokens_input'] ?? 0);
                    $outputTokens = (int) ($msg['tokens_output'] ?? 0);
                    $msgCost = ($inputTokens * 0.000015) + ($outputTokens * 0.000075);
                    $msgModel = $msg['model'] ?? 'claude-opus-4-5-20251101';
                    $msgIsOllama = str_starts_with($msgModel, 'ollama:');
                    $msgModelLabel = $msgIsOllama ? substr($msgModel, 7) : $msgModel;
                    ?>
                            <div class="message-meta">
                                <span class="model-info">Modell: <?= htmlspecialchars($msgModelLabel) ?></span>
                                <?php if (!$msgIsOllama && ($inputTokens > 0 || $outputTokens > 0)): ?>
                                <span class="tokens-info">
                                    <span class="tokens-input" title="Input-Tokens">&darr;<?= number_format($inputTokens) ?></span>
                                    <span class="tokens-output" title="Output-Tokens">&uarr;<?= number_format($outputTokens) ?></span>
                                </span>
                                <span class="cost-info" title="Geschätzte Kosten">~$<?= number_format($msgCost, 4) ?></span>
                                <?php elseif ($msgIsOllama): ?>
                                <span class="tokens-info local">lokal</span>
                                <?php endif; ?>
                            </div>
                        <?php endif; ?>
                    </div>
                </div>
                <?php endforeach; ?>

                <!-- Loading indicator (3 bouncing dots) -->
                <div id="chat-loading" class="chat-message chat-message--assistant chat-loading">
                    <div class="typing-indicator">
                        <span></span><span></span><span></span>
                    </div>
                </div>
            </div>

            <form class="chat-form"
                  hx-post="/chat/<?= $session['uuid'] ?? '' ?>/message"
                  hx-target="#chat-messages"
                  hx-swap="beforeend"
                  hx-indicator="#chat-loading">

                <div class="chat-input-row">
                    <input type="text"
                           name="message"
                           placeholder="Stelle eine Frage..."
                           autocomplete="off"
                           required>
                    <button type="submit">Senden</button>
                </div>

                <div class="chat-options-row">
                    <select name="model">
                        <optgroup label="Anthropic">
                            <option value="claude-opus-4-5-20251101" <?= ($session['model'] ?? 'claude-opus-4-5-20251101') === 'claude-opus-4-5-20251101' ? 'selected' : '' ?>>claude-opus-4-5-20251101</option>
                            <option value="claude-sonnet-4-20250514" <?= ($session['model'] ?? '') === 'claude-sonnet-4-20250514' ? 'selected' : '' ?>>claude-sonnet-4-20250514</option>
                        </optgroup>
                        <optgroup label="Ollama (lokal)">
                            <option value="ollama:gemma3:4b-it-qat" <?= ($session['model'] ?? '') === 'ollama:gemma3:4b-it-qat' ? 'selected' : '' ?>>gemma3:4b-it-qat</option>
                            <option value="ollama:mistral:latest" <?= ($session['model'] ?? '') === 'ollama:mistral:latest' ? 'selected' : '' ?>>mistral:latest</option>
                            <option value="ollama:llama3.2:latest" <?= ($session['model'] ?? '') === 'ollama:llama3.2:latest' ? 'selected' : '' ?>>llama3.2:latest</option>
                            <option value="ollama:gpt-oss:20b" <?= ($session['model'] ?? '') === 'ollama:gpt-oss:20b' ? 'selected' : '' ?>>gpt-oss:20b</option>
                        </optgroup>
                    </select>
                    <select name="collections[]" multiple class="collections-select" title="Collections für RAG (leer = kein RAG)">
                        <?php
                        $selectedCollections = json_decode($session['collections'] ?? '["documents"]', true) ?: ['documents'];
                        foreach ($collections ?? ['documents'] as $col):
                            $label = match ($col) {
                                'documents' => 'Dokumente',
                                'dokumentation_chunks' => 'Doku-Chunks',
                                'mail' => 'E-Mails',
                                default => ucfirst($col),
                            };
                            $isSelected = in_array($col, $selectedCollections, true);
                        ?>
                        <option value="<?= htmlspecialchars($col) ?>" <?= $isSelected ? 'selected' : '' ?>><?= htmlspecialchars($label) ?></option>
                        <?php endforeach; ?>
                    </select>
                    <select name="context_limit">
                        <?php $currentLimit = (int) ($session['context_limit'] ?? 5); ?>
                        <option value="3" <?= $currentLimit === 3 ? 'selected' : '' ?>>3 Quellen</option>
                        <option value="5" <?= $currentLimit === 5 ? 'selected' : '' ?>>5 Quellen</option>
                        <option value="10" <?= $currentLimit === 10 ? 'selected' : '' ?>>10 Quellen</option>
                        <option value="15" <?= $currentLimit === 15 ? 'selected' : '' ?>>15 Quellen</option>
                    </select>
                    <select name="author_profile_id">
                        <?php $currentProfileId = (int) ($session['author_profile_id'] ?? 0); ?>
                        <option value="0" <?= $currentProfileId === 0 ? 'selected' : '' ?>>Kein Profil</option>
                        <?php foreach ($authorProfiles ?? [] as $profile): ?>
                        <option value="<?= $profile['id'] ?>" <?= $currentProfileId === (int) $profile['id'] ? 'selected' : '' ?>><?= htmlspecialchars($profile['name']) ?></option>
                        <?php endforeach; ?>
                    </select>
                    <select name="system_prompt_id">
                        <?php $currentPromptId = (int) ($session['system_prompt_id'] ?? 1); ?>
                        <?php foreach ($systemPrompts ?? [] as $prompt): ?>
                        <option value="<?= $prompt['id'] ?>" <?= $currentPromptId === (int) $prompt['id'] ? 'selected' : '' ?>><?= htmlspecialchars($prompt['name']) ?></option>
                        <?php endforeach; ?>
                    </select>
                </div>
                <div class="chat-options-row chat-options-row--advanced">
                    <?php $currentTemperature = (float) ($session['temperature'] ?? 0.7); ?>
                    <div class="temperature-control">
                        <label for="temperature">Temp: <span id="temperature-value"><?= number_format($currentTemperature, 1) ?></span></label>
                        <input type="range" name="temperature" id="temperature" min="0" max="1" step="0.1" value="<?= $currentTemperature ?>">
                    </div>
                    <select name="max_tokens">
                        <?php $currentMaxTokens = (int) ($session['max_tokens'] ?? 4096); ?>
                        <option value="1024" <?= $currentMaxTokens === 1024 ? 'selected' : '' ?>>1024 Tokens</option>
                        <option value="2048" <?= $currentMaxTokens === 2048 ? 'selected' : '' ?>>2048 Tokens</option>
                        <option value="4096" <?= $currentMaxTokens === 4096 ? 'selected' : '' ?>>4096 Tokens</option>
                        <option value="8192" <?= $currentMaxTokens === 8192 ? 'selected' : '' ?>>8192 Tokens</option>
                    </select>
                    <div class="preset-buttons">
                        <button type="button" class="preset-btn" data-temperature="0.3" data-max-tokens="2048" title="Präzise: niedrige Temperature, weniger Tokens">Präzise</button>
                        <button type="button" class="preset-btn" data-temperature="0.7" data-max-tokens="4096" title="Ausgewogen: Standard-Werte">Ausgewogen</button>
                        <button type="button" class="preset-btn" data-temperature="0.9" data-max-tokens="4096" title="Kreativ: hohe Temperature">Kreativ</button>
                    </div>
                </div>
            </form>
        </div>
    </main>
</div>

<script>
// Sidebar Toggle
document.getElementById('sidebar-toggle')?.addEventListener('click', function() {
    document.getElementById('chat-sidebar').classList.toggle('chat-sidebar--open');
    this.classList.toggle('active');
});

// Close sidebar when clicking outside on mobile
document.querySelector('.chat-main')?.addEventListener('click', function() {
    if (window.innerWidth <= 768) {
        document.getElementById('chat-sidebar').classList.remove('chat-sidebar--open');
        document.getElementById('sidebar-toggle')?.classList.remove('active');
    }
});

// Clear input after request
document.body.addEventListener('htmx:afterRequest', function(evt) {
    if (evt.detail.elt.classList.contains('chat-form')) {
        const input = evt.detail.elt.querySelector('input[name="message"]');
        input.value = '';
        input.focus();
        evt.detail.elt.querySelector('button').disabled = false;
        document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;

        // Refresh session list to update title
        htmx.ajax('GET', '/chat/sessions?current=<?= $session['uuid'] ?? '' ?>', '#session-list');
    }
});

document.body.addEventListener('htmx:afterSwap', function(evt) {
    if (evt.detail.target.id === 'chat-messages') {
        evt.detail.target.scrollTop = evt.detail.target.scrollHeight;
    }
});

document.body.addEventListener('htmx:beforeRequest', function(evt) {
    if (evt.detail.elt.classList.contains('chat-form')) {
        evt.detail.elt.querySelector('button').disabled = true;

        // Move loading indicator to end of messages
        const messages = document.getElementById('chat-messages');
        const loading = document.getElementById('chat-loading');
        if (messages && loading) {
            messages.appendChild(loading);
            messages.scrollTop = messages.scrollHeight;
        }
    }
});

// Sources Toggle
function toggleSources(id) {
    const el = document.getElementById(id);
    if (!el) return;
    el.classList.toggle('chat-sources--collapsed');

    // Save preference to localStorage
    const isExpanded = !el.classList.contains('chat-sources--collapsed');
    localStorage.setItem('chat-sources-expanded', isExpanded ? '1' : '0');
}

// Apply saved preferences on load
document.addEventListener('DOMContentLoaded', function() {
    // Sources toggle preference
    const preference = localStorage.getItem('chat-sources-expanded');
    if (preference === '1') {
        document.querySelectorAll('.chat-sources--collapsed').forEach(el => {
            el.classList.remove('chat-sources--collapsed');
        });
    }

    // Restore dropdown values from localStorage
    const dropdowns = ['model', 'collection', 'context_limit', 'author_profile_id', 'system_prompt_id', 'max_tokens'];
    dropdowns.forEach(name => {
        const saved = localStorage.getItem('chat_' + name);
        const select = document.querySelector('select[name="' + name + '"]');
        if (saved && select) {
            select.value = saved;
        }
    });

    // Save dropdown values on change
    dropdowns.forEach(name => {
        const select = document.querySelector('select[name="' + name + '"]');
        if (select) {
            select.addEventListener('change', function() {
                localStorage.setItem('chat_' + name, this.value);
            });
        }
    });

    // Temperature slider
    const tempSlider = document.getElementById('temperature');
    const tempValue = document.getElementById('temperature-value');
    if (tempSlider && tempValue) {
        // Restore saved temperature
        const savedTemp = localStorage.getItem('chat_temperature');
        if (savedTemp) {
            tempSlider.value = savedTemp;
            tempValue.textContent = parseFloat(savedTemp).toFixed(1);
        }

        // Update display on change
        tempSlider.addEventListener('input', function() {
            tempValue.textContent = parseFloat(this.value).toFixed(1);
        });

        // Save on change
        tempSlider.addEventListener('change', function() {
            localStorage.setItem('chat_temperature', this.value);
        });
    }

    // Preset buttons
    document.querySelectorAll('.preset-btn').forEach(btn => {
        btn.addEventListener('click', function() {
            const temp = this.dataset.temperature;
            const tokens = this.dataset.maxTokens;

            // Set temperature
            if (tempSlider && tempValue) {
                tempSlider.value = temp;
                tempValue.textContent = parseFloat(temp).toFixed(1);
                localStorage.setItem('chat_temperature', temp);
            }

            // Set max tokens
            const tokensSelect = document.querySelector('select[name="max_tokens"]');
            if (tokensSelect) {
                tokensSelect.value = tokens;
                localStorage.setItem('chat_max_tokens', tokens);
            }

            // Visual feedback
            document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('preset-btn--active'));
            this.classList.add('preset-btn--active');
        });
    });
});

// Inline Title Edit
function editSessionTitle(uuid) {
    const titleEl = document.getElementById('title-' + uuid);
    if (!titleEl) return;

    const currentTitle = titleEl.textContent;
    const input = document.createElement('input');
    input.type = 'text';
    input.value = currentTitle;
    input.className = 'session-item__title-input';
    input.maxLength = 100;

    titleEl.innerHTML = '';
    titleEl.appendChild(input);
    input.focus();
    input.select();

    function saveTitle() {
        const newTitle = input.value.trim() || 'Neuer Chat';
        titleEl.textContent = newTitle;

        // Save via HTMX
        fetch('/chat/' + uuid + '/title', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: 'title=' + encodeURIComponent(newTitle)
        }).then(response => response.text())
          .then(html => {
              titleEl.textContent = html || newTitle;
              // Update page title if current session
              const pageTitle = document.querySelector('.chat-header h1');
              if (pageTitle && window.location.pathname.includes(uuid)) {
                  pageTitle.textContent = html || newTitle;
              }
          });
    }

    input.addEventListener('blur', saveTitle);
    input.addEventListener('keydown', function(e) {
        if (e.key === 'Enter') {
            e.preventDefault();
            input.blur();
        }
        if (e.key === 'Escape') {
            titleEl.textContent = currentTitle;
        }
    });
}
</script>

<?php $content = ob_get_clean(); ?>
<?php require VIEW_PATH . '/layout.php'; ?>

Vollständig herunterladen

Aktionen

Herunterladen

Andere Versionen dieser Datei

ID Version Typ Größe Datum
121 12 modified 22.2 KB 2025-12-20 19:27
120 11 modified 22.1 KB 2025-12-20 19:27
119 10 modified 22.1 KB 2025-12-20 19:27
118 9 modified 22.2 KB 2025-12-20 19:27
90 8 modified 21.4 KB 2025-12-20 19:16
88 7 modified 21.2 KB 2025-12-20 19:16
73 6 modified 20.9 KB 2025-12-20 19:04
72 5 modified 20.8 KB 2025-12-20 19:00
70 4 modified 20.5 KB 2025-12-20 18:59
69 3 modified 20.5 KB 2025-12-20 18:49
62 2 modified 18.7 KB 2025-12-20 18:33
61 1 modified 17.0 KB 2025-12-20 18:32

← Zurück zur Übersicht