- eval mit gpt-6-luna
- new chat interface with personas
This commit is contained in:
+593
-191
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user