- eval mit gpt-6-luna

- new chat interface with personas
This commit is contained in:
2026-10-08 10:06:32 +02:00
parent e75716714e
commit 30fd55da21
17 changed files with 876 additions and 315 deletions
+593 -191
View File
@@ -1,217 +1,559 @@
<!DOCTYPE html>
<html lang="en">
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Local RAG Chat</title>
<title>Im Arbeitszimmer · Gespräch über Kafka</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;1,500&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;1,8..60,400&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
/* Farben aus dem Bild: Nacht im Arbeitszimmer, Lampenlicht, Papier, Prager Dämmerung */
:root {
--ink: #0d0a08;
--umber-900: #14100c;
--umber-800: #1c1611;
--umber-700: #261e18;
--umber-600: #382b21;
--sepia: #8a6852;
--paper: #ecdcc0;
--paper-dim: #b9a586;
--paper-faint:#7d6c58;
--lamp: #f2b45a;
--lamp-deep: #c98a3a;
--lamp-glow: rgba(242, 180, 90, 0.16);
--dusk: #5d6280;
--ember: #d9824a;
--line: rgba(236, 220, 192, 0.09);
--avatar: url("/visuals/researcher_avatar.jpg"); /* wird je Gesprächspartner gesetzt */
--display: "Cormorant Garamond", "Cormorant", Georgia, serif;
--serif: "Source Serif 4", "Source Serif Pro", Georgia, serif;
--mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
font-family: "SF Mono", "Consolas", "Monaco", monospace;
margin: 0;
height: 100vh;
overflow: hidden;
background: #0f0f12;
color: #e4e4e7;
background: var(--ink);
color: var(--paper);
font-family: var(--serif);
-webkit-font-smoothing: antialiased;
display: grid;
grid-template-columns: minmax(380px, 46%) 1fr;
}
/* ---------- Bild ---------- */
.portrait {
position: relative;
overflow: hidden;
background: var(--umber-900);
}
.portrait img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: 60% 40%;
transform: scale(1.02);
transition: opacity .35s ease;
}
.portrait img.switching { opacity: 0; }
/* sanfter Übergang ins Gespräch und Abdunklung unten für die Bildunterschrift */
.portrait::after {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(to right, transparent 72%, var(--ink) 100%),
linear-gradient(to top, rgba(13,10,8,0.92) 0%, rgba(13,10,8,0.55) 22%, transparent 48%),
radial-gradient(ellipse at 92% 54%, rgba(242,180,90,0.10), transparent 40%);
pointer-events: none;
}
.portrait figcaption {
position: absolute;
left: clamp(1.5rem, 4vw, 3rem);
right: 3rem;
bottom: clamp(1.5rem, 5vh, 3rem);
z-index: 1;
}
.place {
font-family: var(--mono);
font-size: 0.7rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--lamp);
margin: 0 0 0.6rem;
}
.portrait h1 {
font-family: var(--display);
font-weight: 600;
font-size: clamp(2.4rem, 4.2vw, 3.6rem);
line-height: 1;
letter-spacing: -0.01em;
margin: 0;
color: #f6ead3;
text-shadow: 0 2px 24px rgba(0,0,0,0.6);
}
.role {
font-family: var(--display);
font-style: italic;
font-size: clamp(1.1rem, 1.6vw, 1.35rem);
color: var(--paper-dim);
margin: 0.5rem 0 0;
}
.status {
display: inline-flex;
align-items: center;
gap: 0.5rem;
margin-top: 1rem;
font-family: var(--mono);
font-size: 0.7rem;
letter-spacing: 0.08em;
color: var(--paper-dim);
}
.status::before {
content: "";
width: 7px; height: 7px;
border-radius: 50%;
background: var(--lamp);
box-shadow: 0 0 10px 2px rgba(242,180,90,0.55);
}
/* ---------- Gespräch ---------- */
.chat {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
height: 100vh;
background:
radial-gradient(ellipse at 100% 0%, rgba(93,98,128,0.10), transparent 45%),
radial-gradient(ellipse at 0% 100%, rgba(242,180,90,0.05), transparent 50%),
var(--ink);
}
header {
.chat-head {
flex-shrink: 0;
padding: 1rem 1.5rem;
border-bottom: 1px solid #27272a;
background: #18181b;
display: flex;
align-items: center;
gap: 0.9rem;
padding: 1.1rem clamp(1.25rem, 3vw, 2.5rem);
border-bottom: 1px solid var(--line);
}
header h1 {
margin: 0;
font-size: 1rem;
.chat-head .who { min-width: 0; }
.chat-head .who strong {
display: block;
font-family: var(--display);
font-weight: 600;
letter-spacing: 0.02em;
font-size: 1.3rem;
line-height: 1.1;
color: #f3e5cb;
}
header p {
margin: 0.25rem 0 0;
font-size: 0.75rem;
color: #71717a;
.chat-head .who span {
white-space: nowrap;
font-family: var(--mono);
font-size: 0.68rem;
letter-spacing: 0.06em;
color: var(--paper-faint);
}
.selectors { margin-left: auto; display: flex; align-items: center; gap: 1.1rem; }
.model {
white-space: nowrap;
display: flex;
align-items: center;
gap: 0.5rem;
font-family: var(--mono);
font-size: 0.68rem;
letter-spacing: 0.06em;
color: var(--paper-faint);
}
.model select {
font: inherit;
font-size: 0.72rem;
color: var(--paper);
background: var(--umber-800);
border: 1px solid var(--line);
border-radius: 999px;
padding: 0.25rem 0.55rem;
cursor: pointer;
}
.avatar {
flex-shrink: 0;
width: 44px; height: 44px;
border-radius: 50%;
background: var(--avatar) center / cover, var(--umber-700);
box-shadow: 0 0 0 1.5px var(--lamp-deep), 0 0 18px rgba(242,180,90,0.18);
}
.avatar.small { width: 36px; height: 36px; }
#messages {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 1.5rem;
padding: clamp(1.5rem, 4vh, 2.5rem) clamp(1.25rem, 3vw, 2.5rem) 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
gap: 1.4rem;
scrollbar-width: thin;
scrollbar-color: var(--umber-600) transparent;
}
.msg {
max-width: 85%;
padding: 0.75rem 1rem;
border-radius: 8px;
font-size: 0.9rem;
line-height: 1.5;
.row { display: flex; gap: 0.85rem; align-items: flex-start; max-width: 46rem; }
.row.assistant { align-self: flex-start; }
.row.user { align-self: flex-end; justify-content: flex-end; }
.bubble {
min-width: 0;
padding: 1rem 1.25rem;
border-radius: 4px 14px 14px 14px;
font-size: 1.02rem;
line-height: 1.65;
word-break: break-word;
}
.msg.user, .msg.error {
.row.assistant .bubble {
background: linear-gradient(160deg, #241c15, #19130e);
border: 1px solid var(--line);
border-left: 2px solid var(--lamp-deep);
color: var(--paper);
box-shadow: 0 10px 30px -18px rgba(0,0,0,0.9);
}
.row.user .bubble {
border-radius: 14px 4px 14px 14px;
background: var(--lamp-glow);
border: 1px solid rgba(242,180,90,0.28);
color: #f6e9d1;
white-space: pre-wrap;
}
.msg.assistant .markdown-body {
white-space: normal;
.row.error .bubble {
background: #2a1210;
border: 1px solid #6b2a20;
border-left: 2px solid var(--ember);
color: #f3c8b6;
white-space: pre-wrap;
}
.msg.assistant .markdown-body h1, .msg.assistant .markdown-body h2, .msg.assistant .markdown-body h3 {
margin: 0.75em 0 0.35em;
font-size: 1em;
font-weight: 600;
}
.msg.assistant .markdown-body h1:first-child, .msg.assistant .markdown-body h2:first-child, .msg.assistant .markdown-body h3:first-child { margin-top: 0; }
.msg.assistant .markdown-body p { margin: 0.5em 0; }
.msg.assistant .markdown-body p:first-child { margin-top: 0; }
.msg.assistant .markdown-body p:last-child { margin-bottom: 0; }
.msg.assistant .markdown-body pre {
margin: 0.5em 0;
padding: 0.6rem;
background: #18181b;
border-radius: 6px;
overflow-x: auto;
font-size: 0.85em;
}
.msg.assistant .markdown-body code {
background: #18181b;
padding: 0.15em 0.35em;
border-radius: 4px;
font-size: 0.9em;
}
.msg.assistant .markdown-body pre code {
padding: 0;
background: none;
}
.msg.assistant .markdown-body ul, .msg.assistant .markdown-body ol { margin: 0.5em 0; padding-left: 1.4em; }
.msg.assistant .markdown-body li { margin: 0.25em 0; }
.msg.assistant .markdown-body a { color: #60a5fa; text-decoration: none; }
.msg.assistant .markdown-body a:hover { text-decoration: underline; }
.msg.user {
align-self: flex-end;
background: #3f3f46;
color: #fafafa;
}
.msg.assistant {
align-self: flex-start;
background: #27272a;
border: 1px solid #3f3f46;
}
.msg.error {
background: #451a1a;
border: 1px solid #7f1d1d;
color: #fecaca;
}
.msg .label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #71717a;
margin-bottom: 0.5rem;
.who-label {
display: block;
font-family: var(--display);
font-style: italic;
font-size: 1rem;
color: var(--lamp);
margin-bottom: 0.3rem;
}
#input-area {
.row.user .who-label { color: var(--paper-dim); text-align: right; }
/* Markdown in Antworten */
.md p { margin: 0.55em 0; }
.md p:first-child { margin-top: 0; }
.md p:last-child { margin-bottom: 0; }
.md h1, .md h2, .md h3 {
font-family: var(--display);
font-weight: 600;
font-size: 1.25em;
margin: 0.9em 0 0.3em;
color: #f3e5cb;
}
.md h1:first-child, .md h2:first-child, .md h3:first-child { margin-top: 0; }
.md ul, .md ol { margin: 0.55em 0; padding-left: 1.3em; }
.md li { margin: 0.25em 0; }
.md li::marker { color: var(--lamp-deep); }
.md strong { color: #f6e9d1; font-weight: 600; }
.md em { color: #f0dfbf; }
.md blockquote {
margin: 0.8em 0;
padding: 0.1em 0 0.1em 1em;
border-left: 2px solid var(--sepia);
color: var(--paper-dim);
font-style: italic;
}
.md a { color: var(--lamp); text-decoration-thickness: 1px; text-underline-offset: 3px; }
.md code {
font-family: var(--mono);
font-size: 0.85em;
background: rgba(0,0,0,0.35);
padding: 0.1em 0.35em;
border-radius: 3px;
}
.md pre { overflow-x: auto; background: rgba(0,0,0,0.35); padding: 0.7rem; border-radius: 6px; }
.md pre code { background: none; padding: 0; }
/* Quellen unter einer Antwort */
.sources {
margin-top: 0.9rem;
padding-top: 0.7rem;
border-top: 1px dashed rgba(236,220,192,0.14);
font-family: var(--mono);
font-size: 0.72rem;
color: var(--paper-faint);
}
.sources summary {
cursor: pointer;
list-style: none;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--paper-dim);
}
.sources summary::-webkit-details-marker { display: none; }
.sources summary::before { content: "▸ "; color: var(--lamp-deep); }
.sources[open] summary::before { content: "▾ "; }
.sources ol { margin: 0.6rem 0 0; padding-left: 1.4rem; }
.sources li { margin: 0.3rem 0; line-height: 1.45; }
.sources .src-title { color: var(--paper-dim); }
.sources .src-page { color: var(--lamp-deep); white-space: nowrap; }
.sources .searched { margin: 0.6rem 0 0; font-style: italic; font-family: var(--serif); font-size: 0.85rem; }
/* Beispielfragen */
.suggestions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.2rem 0 0 calc(44px + 0.85rem);
max-width: 46rem;
}
.suggestions button {
font-family: var(--serif);
font-size: 0.9rem;
font-style: italic;
color: var(--paper-dim);
background: transparent;
border: 1px solid rgba(236,220,192,0.16);
border-radius: 999px;
padding: 0.45rem 0.95rem;
cursor: pointer;
transition: border-color .2s, color .2s, background .2s;
}
.suggestions button:hover {
color: var(--paper);
border-color: var(--lamp-deep);
background: rgba(242,180,90,0.06);
}
/* Ladeanzeige: Lampe glimmt */
.thinking .bubble { font-style: italic; color: var(--paper-dim); }
.thinking .pending { display: flex; align-items: center; gap: 0.7rem; }
.glow {
width: 9px; height: 9px;
border-radius: 50%;
background: var(--lamp);
box-shadow: 0 0 12px 3px rgba(242,180,90,0.5);
animation: glow 1.6s ease-in-out infinite;
}
@keyframes glow {
0%, 100% { opacity: 0.35; transform: scale(0.85); }
50% { opacity: 1; transform: scale(1.1); }
}
/* ---------- Eingabe ---------- */
.composer {
flex-shrink: 0;
padding: 1rem 1.5rem 1.5rem;
border-top: 1px solid #27272a;
background: #18181b;
padding: 0.9rem clamp(1.25rem, 3vw, 2.5rem) 1.3rem;
border-top: 1px solid var(--line);
background: linear-gradient(to top, var(--umber-900), transparent);
}
#provider-row {
.field {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
font-size: 0.8rem;
color: #71717a;
}
#provider-row label { flex-shrink: 0; }
#provider {
padding: 0.25rem 0.5rem;
font: inherit;
font-size: 0.85rem;
color: #e4e4e7;
background: #27272a;
border: 1px solid #3f3f46;
border-radius: 6px;
}
#input-row {
display: flex;
gap: 0.5rem;
align-items: flex-end;
gap: 0.6rem;
padding: 0.45rem 0.45rem 0.45rem 1.1rem;
background: var(--umber-800);
border: 1px solid rgba(236,220,192,0.12);
border-radius: 18px;
transition: border-color .2s, box-shadow .2s;
}
.field:focus-within {
border-color: rgba(242,180,90,0.5);
box-shadow: 0 0 0 4px rgba(242,180,90,0.07);
}
#input {
flex: 1;
min-height: 44px;
height: 44px;
max-height: 160px;
padding: 0.6rem 1rem;
font: inherit;
font-size: 0.9rem;
color: #e4e4e7;
background: #27272a;
border: 1px solid #3f3f46;
border-radius: 8px;
resize: none;
outline: none;
}
#input::placeholder { color: #71717a; }
#input:focus { border-color: #52525b; }
#send {
height: 44px;
padding: 0 1.2rem;
font: inherit;
font-size: 0.85rem;
font-weight: 500;
color: #0f0f12;
background: #e4e4e7;
min-height: 2.6rem;
max-height: 10rem;
padding: 0.6rem 0;
font-family: var(--serif);
font-size: 1.02rem;
line-height: 1.45;
color: var(--paper);
background: transparent;
border: none;
border-radius: 8px;
outline: none;
resize: none;
}
#input::placeholder { color: var(--paper-faint); font-style: italic; }
#send {
flex-shrink: 0;
width: 2.6rem; height: 2.6rem;
display: grid;
place-items: center;
border: none;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #ffd08a, var(--lamp) 45%, var(--lamp-deep));
color: #2a1a0b;
cursor: pointer;
box-shadow: 0 0 18px rgba(242,180,90,0.35);
transition: transform .15s, opacity .2s;
}
#send:hover { background: #fafafa; }
#send:disabled {
opacity: 0.5;
cursor: not-allowed;
#send:hover { transform: translateY(-1px); }
#send:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
.hint {
margin: 0.55rem 0.3rem 0;
font-family: var(--mono);
font-size: 0.64rem;
letter-spacing: 0.05em;
color: var(--paper-faint);
}
.loading {
align-self: flex-start;
padding: 0.5rem 1rem;
font-size: 0.8rem;
color: #71717a;
:focus-visible { outline: 2px solid var(--lamp); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
.glow { animation: none; opacity: 0.8; }
#send, .suggestions button { transition: none; }
}
/* ---------- schmale Bildschirme: Bild als Kopf ---------- */
@media (max-width: 900px) {
body { display: flex; flex-direction: column; overflow: hidden; }
.portrait { flex: 0 0 auto; height: 36vh; min-height: 220px; }
.portrait::after {
background:
linear-gradient(to top, var(--ink) 0%, rgba(13,10,8,0.6) 35%, transparent 70%);
}
.portrait figcaption { bottom: 1rem; left: 1.1rem; }
.portrait h1 { font-size: 2rem; }
.role { font-size: 1rem; margin-top: 0.25rem; }
.status, .place { display: none; }
.chat { height: auto; flex: 1; min-height: 0; }
.chat-head { padding: 0.45rem 1rem; justify-content: flex-end; }
.chat-head .avatar, .chat-head .who { display: none; }
/* kompakte Menüs rechts, Beschriftung nur für Screenreader */
.selectors { margin-left: auto; gap: 0.5rem; }
.model { font-size: 0; }
.model select { font-size: 0.72rem; }
.hint { display: none; }
#messages { padding: 1rem; }
.row { max-width: 100%; }
.avatar.small { width: 30px; height: 30px; }
.suggestions { margin-left: 0; }
.bubble { font-size: 0.98rem; padding: 0.85rem 1rem; }
.composer { padding: 0.7rem 1rem 1rem; }
}
</style>
<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>
</head>
<body>
<header>
<h1>RAG Chat</h1>
<p>Ask questions about your documents. Answers are generated from a local vector store + Ollama / OpenAI.</p>
</header>
<figure class="portrait" style="margin:0">
<img id="portrait-img" src="/visuals/kafka_researcher_office.jpg" alt="" />
<figcaption>
<p class="place" id="persona-place"></p>
<h1 id="persona-name"></h1>
<p class="role" id="persona-role"></p>
<span class="status" id="persona-status"></span>
</figcaption>
</figure>
<div id="messages"></div>
<main class="chat">
<header class="chat-head">
<div class="avatar" aria-hidden="true"></div>
<div class="who">
<strong id="head-name"></strong>
<span>Gespräch über Franz Kafka</span>
</div>
<div class="selectors">
<label class="model">
Gegenüber
<select id="persona"></select>
</label>
<label class="model">
Modell
<select id="provider">
<option value="ollama">Ollama</option>
<option value="openai">ChatGPT</option>
</select>
</label>
</div>
</header>
<div id="input-area">
<div id="provider-row">
<label for="provider">LLM provider:</label>
<select id="provider">
<option value="ollama">Ollama</option>
<option value="openai">OpenAI</option>
</select>
<div id="messages" aria-live="polite"></div>
<div class="composer">
<div class="field">
<textarea id="input" rows="1" autofocus></textarea>
<button type="button" id="send" aria-label="Frage senden">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M5 12h13M13 6l6 6-6 6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<p class="hint">Enter sendet · Shift + Enter für eine neue Zeile · Antworten stützen sich auf die Texte im lokalen Korpus</p>
</div>
<div id="input-row">
<textarea id="input" rows="1" placeholder="Ask a question…" autofocus></textarea>
<button type="button" id="send">Send</button>
</div>
</div>
</main>
<script>
// Gesprächspartner (Texte, Bilder) kommen vom Server: personas.py, /api/personas
let PERSONAS = {};
let PERSONA = null;
const messagesEl = document.getElementById('messages');
const inputEl = document.getElementById('input');
const sendBtn = document.getElementById('send');
const providerEl = document.getElementById('provider');
const personaEl = document.getElementById('persona');
const portraitEl = document.getElementById('portrait-img');
const chatHistory = [];
// Gesprächspartner wechseln: Bild, Texte und Avatar tauschen, Gespräch neu beginnen
function applyPersona(id) {
PERSONA = PERSONAS[id];
personaEl.value = id;
document.title = PERSONA.name + ' · Gespräch über Kafka';
document.documentElement.style.setProperty('--avatar', 'url("' + PERSONA.avatar + '")');
document.getElementById('persona-place').textContent = PERSONA.place;
document.getElementById('persona-name').textContent = PERSONA.name;
document.getElementById('persona-role').textContent = PERSONA.role;
document.getElementById('persona-status').textContent = PERSONA.status;
document.getElementById('head-name').textContent = PERSONA.name;
inputEl.placeholder = 'Ihre Frage an ' + PERSONA.name + ' …';
portraitEl.classList.add('switching');
const img = new Image();
img.onload = () => {
portraitEl.src = PERSONA.image;
portraitEl.alt = PERSONA.image_alt;
portraitEl.style.objectPosition = PERSONA.image_position;
portraitEl.classList.remove('switching');
};
img.src = PERSONA.image;
messagesEl.innerHTML = '';
chatHistory.length = 0;
// Begrüßung (nicht Teil des Gesprächsverlaufs, der an das Modell geht)
appendMessage('assistant', PERSONA.greeting, { untracked: true });
showSuggestions();
messagesEl.scrollTop = 0;
}
(async function initPersonas() {
const res = await fetch('/api/personas');
const data = await res.json();
data.personas.forEach(p => {
PERSONAS[p.id] = p;
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.label;
personaEl.appendChild(opt);
});
applyPersona(data.default);
})();
personaEl.addEventListener('change', () => applyPersona(personaEl.value));
(async function initProviders() {
try {
const res = await fetch('/api/providers');
@@ -219,55 +561,115 @@
if (!data.openai) {
const opt = providerEl.querySelector('option[value="openai"]');
opt.disabled = true;
opt.textContent = 'OpenAI (not configured)';
opt.textContent = 'ChatGPT (aus)';
opt.title = 'OpenAI ist nicht eingerichtet (OPENAI_API_KEY fehlt)';
if (providerEl.value === 'openai') providerEl.value = 'ollama';
}
} catch (_) {}
})();
function appendMessage(role, text, isError = false, llmProvider = null) {
text = text ?? '';
const div = document.createElement('div');
div.className = 'msg ' + (isError ? 'error' : role);
let label = role === 'user' ? 'You' : (llmProvider === 'openai' ? 'ChatGPT + RAG' : 'Ollama + RAG');
let body;
if (role === 'assistant' && !isError) {
const rawHtml = marked.parse(text, { gfm: true, breaks: true });
body = '<div class="markdown-body">' + DOMPurify.sanitize(rawHtml) + '</div>';
} else {
body = escapeHtml(text);
}
div.innerHTML = '<span class="label">' + label + '</span>' + body;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
chatHistory.push({ role: role, content: text });
}
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function renderMarkdown(text) {
return DOMPurify.sanitize(marked.parse(text, { gfm: true, breaks: true }));
}
// "Binder,_Hartmut_KafkaHandbuch_Band2_AText.pdf" -> "Binder, Hartmut: Kafka Handbuch – Band2 – A Text"
function sourceLabel(path) {
const stem = (path || '').split('/').pop().replace(/\.pdf$/i, '').replace(/[_ .]+$/, '');
const tokens = stem.split('_').filter(Boolean);
const authors = [];
let i = 0;
while (i < tokens.length && tokens[i].endsWith(',')) {
authors.push(tokens[i].slice(0, -1) + (tokens[i + 1] ? ', ' + tokens[i + 1] : ''));
i += 2;
}
const title = tokens.slice(i).map(t => t.replace(/([a-zäöüß])([A-ZÄÖÜ])/g, '$1 $2')).join(' – ');
if (!authors.length) return title || stem;
return authors.join('; ') + (title ? ': ' + title : '');
}
// Quellen, die das Gegenüber über das Such-Werkzeug nachgeschlagen hat (keine Suche: keine Liste)
function sourcesHtml(retrieved, searches) {
if (!retrieved || !retrieved.length) return '';
const items = retrieved.map(c => {
const page = c.page != null ? 'S. ' + (c.page + 1) : '';
return '<li><span class="src-title">' + escapeHtml(sourceLabel(c.source)) + '</span> ' +
'<span class="src-page">' + page + '</span></li>';
}).join('');
const searched = (searches && searches.length)
? '<p class="searched">nachgeschlagen: ' + searches.map(q => '„' + escapeHtml(q) + '“').join(', ') + '</p>'
: '';
return '<details class="sources"><summary>Quellen (' + retrieved.length + ')</summary>' + searched +
'<ol>' + items + '</ol></details>';
}
function addRow(kind, innerHtml, label) {
const row = document.createElement('div');
row.className = 'row ' + kind;
const avatar = kind === 'user' ? '' : '<div class="avatar small" aria-hidden="true"></div>';
row.innerHTML = avatar + '<div class="bubble"><span class="who-label">' + label + '</span>' + innerHtml + '</div>';
messagesEl.appendChild(row);
messagesEl.scrollTop = messagesEl.scrollHeight;
return row;
}
function appendMessage(role, text, opts = {}) {
text = text ?? '';
if (opts.error) {
addRow('error assistant', escapeHtml(text), 'Hinweis');
} else if (role === 'assistant') {
addRow('assistant', '<div class="md">' + renderMarkdown(text) + '</div>' + sourcesHtml(opts.retrieved, opts.searches),
escapeHtml(PERSONA.name));
} else {
addRow('user', escapeHtml(text), 'Sie');
}
if (!opts.untracked) chatHistory.push({ role: role, content: text });
}
function setLoading(show) {
if (show) {
const el = document.createElement('div');
el.className = 'loading';
el.id = 'loading-dot';
el.textContent = 'Thinking…';
messagesEl.appendChild(el);
messagesEl.scrollTop = messagesEl.scrollHeight;
const row = addRow('assistant thinking', '<span class="pending"><span class="glow" aria-hidden="true"></span>überlegt …</span>',
escapeHtml(PERSONA.name));
row.id = 'thinking';
} else {
const el = document.getElementById('loading-dot');
const el = document.getElementById('thinking');
if (el) el.remove();
}
sendBtn.disabled = show;
personaEl.disabled = show; // während einer Antwort nicht wechseln
}
function showSuggestions() {
const box = document.createElement('div');
box.className = 'suggestions';
box.id = 'suggestions';
PERSONA.suggestions.forEach(q => {
const b = document.createElement('button');
b.type = 'button';
b.textContent = q;
b.addEventListener('click', () => { inputEl.value = q; send(); });
box.appendChild(b);
});
messagesEl.appendChild(box);
}
function autosize() {
inputEl.style.height = 'auto';
inputEl.style.height = Math.min(inputEl.scrollHeight, 160) + 'px';
}
async function send() {
const text = inputEl.value.trim();
if (!text) return;
if (!text || sendBtn.disabled) return;
inputEl.value = '';
autosize();
const sugg = document.getElementById('suggestions');
if (sugg) sugg.remove();
appendMessage('user', text);
setLoading(true);
@@ -276,17 +678,14 @@
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: text,
history: history,
llm_provider: providerEl.value
})
body: JSON.stringify({ message: text, history: history, llm_provider: providerEl.value,
persona: personaEl.value })
});
const data = await res.json();
setLoading(false);
if (data.retrieved && data.retrieved.length > 0) {
console.groupCollapsed('[RAG] Retrieved ' + data.retrieved.length + ' chunk(s)');
data.retrieved.forEach(function(chunk, i) {
data.retrieved.forEach(function (chunk, i) {
console.group('Chunk ' + (i + 1) + ' | ' + (chunk.source || '') + (chunk.page != null ? ' p.' + chunk.page : ''));
console.log(chunk.content);
console.groupEnd();
@@ -294,17 +693,20 @@
console.groupEnd();
}
if (data.error) {
appendMessage('assistant', data.error, true, providerEl.value);
appendMessage('assistant', data.error, { error: true, untracked: true });
} else {
appendMessage('assistant', (data.answer || '(No response)').trim(), false, providerEl.value);
appendMessage('assistant', (data.answer || '(Keine Antwort)').trim(),
{ retrieved: data.retrieved, searches: data.searches });
}
} catch (err) {
setLoading(false);
appendMessage('assistant', 'Request failed: ' + err.message, true);
appendMessage('assistant', 'Die Anfrage ist fehlgeschlagen: ' + err.message, { error: true, untracked: true });
}
inputEl.focus();
}
sendBtn.addEventListener('click', send);
inputEl.addEventListener('input', autosize);
inputEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB