Backup #72
| ID | 72 |
| Dateipfad | /var/www/dev.campus.systemische-tools.de/src/View/chat/index.php |
| Version | 5 |
| Typ |
modified |
| Größe | 20.8 KB |
| Hash | 1b3855b5732af7096414c61c3bfe7a1d4e7cc34422b151fc53c2fe210bdc4673 |
| Datum | 2025-12-20 19:00:14 |
| Geändert von | claude-code-hook |
| Grund | Claude 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">
✎
</button>
<button class="session-item__delete"
hx-delete="/chat/<?= $s['uuid'] ?>"
hx-confirm="Session wirklich löschen?"
onclick="event.preventDefault(); event.stopPropagation();">
×
</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">▼</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">↓<?= number_format($inputTokens) ?></span>
<span class="tokens-output" title="Output-Tokens">↑<?= 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; ?>
</div>
<!-- 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>
<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="collection">
<?php
$currentCollection = $session['collection'] ?? 'documents';
foreach ($collections ?? ['documents'] as $col):
$label = match ($col) {
'documents' => 'Dokumente',
'dokumentation_chunks' => 'Doku-Chunks',
'mail' => 'E-Mails',
default => ucfirst($col),
};
?>
<option value="<?= htmlspecialchars($col) ?>" <?= $currentCollection === $col ? '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;
}
});
// 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
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