first commit
This commit is contained in:
159
public/assets/js/base.js
Normal file
159
public/assets/js/base.js
Normal file
@@ -0,0 +1,159 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chatEl = document.getElementById('chat');
|
||||
const promptEl = document.getElementById('prompt');
|
||||
const sendBtn = document.getElementById('send');
|
||||
const abortBtn = document.getElementById('abort');
|
||||
const clearBtn = document.getElementById('clear');
|
||||
|
||||
let abort = false;
|
||||
|
||||
marked.setOptions({ breaks: true });
|
||||
|
||||
function renderMarkdown(text) {
|
||||
return DOMPurify.sanitize(marked.parse(text));
|
||||
}
|
||||
|
||||
function addMessage(role, html = '', extra = '') {
|
||||
const msg = document.createElement('div');
|
||||
msg.className = 'message ' + role;
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'bubble ' + extra;
|
||||
bubble.innerHTML = html;
|
||||
|
||||
msg.appendChild(bubble);
|
||||
chatEl.appendChild(msg);
|
||||
chatEl.scrollTop = chatEl.scrollHeight;
|
||||
|
||||
return bubble;
|
||||
}
|
||||
|
||||
function addLoader() {
|
||||
return addMessage('assistant', 'AI is thinking…', 'loader');
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const res = await fetch('/history');
|
||||
if (!res.ok) return;
|
||||
const messages = await res.json();
|
||||
messages.forEach(m =>
|
||||
addMessage(m.role, renderMarkdown(m.text))
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
loadHistory();
|
||||
|
||||
sendBtn.addEventListener('click', async () => {
|
||||
const prompt = promptEl.value.trim();
|
||||
if (!prompt) return;
|
||||
|
||||
addMessage('user', renderMarkdown(prompt));
|
||||
promptEl.value = '';
|
||||
|
||||
const bubble = addLoader();
|
||||
let raw = '';
|
||||
let firstChunk = true;
|
||||
let renderTimer = null;
|
||||
|
||||
// 🔥 LÖSUNG: Throttled Rendering - maximal alle 100ms
|
||||
function scheduleRender() {
|
||||
if (renderTimer) return;
|
||||
|
||||
renderTimer = setTimeout(() => {
|
||||
// Der StreamChunker sendet bereits korrekt strukturierte Chunks
|
||||
bubble.innerHTML = renderMarkdown(raw);
|
||||
chatEl.scrollTop = chatEl.scrollHeight;
|
||||
renderTimer = null;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
abort = false;
|
||||
sendBtn.disabled = true;
|
||||
abortBtn.disabled = false;
|
||||
clearBtn.disabled = true;
|
||||
|
||||
try {
|
||||
document.getElementById('ai-cloud')?.classList.remove('d-none');
|
||||
|
||||
const res = await fetch('/ask-sse', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt })
|
||||
});
|
||||
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let sseBuffer = '';
|
||||
|
||||
while (!abort) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
sseBuffer += chunk;
|
||||
|
||||
// Parse SSE Events (können mehrere data:-Zeilen haben)
|
||||
const events = sseBuffer.split('\n\n');
|
||||
sseBuffer = events.pop() || ''; // Letzten unvollständigen Event behalten
|
||||
|
||||
events.forEach(event => {
|
||||
if (!event.trim()) return;
|
||||
|
||||
// Sammle alle "data:"-Zeilen und füge \n wieder ein
|
||||
const dataLines = event
|
||||
.split('\n')
|
||||
.filter(line => line.startsWith('data: '))
|
||||
.map(line => line.slice(6));
|
||||
|
||||
if (dataLines.length === 0) return;
|
||||
|
||||
// Verbinde mit \n (so wie es vom Backend kam)
|
||||
const text = dataLines.join('\n');
|
||||
|
||||
if (text === '[DONE]') {
|
||||
// Finales Rendering mit Normalisierung
|
||||
if (renderTimer) {
|
||||
clearTimeout(renderTimer);
|
||||
renderTimer = null;
|
||||
}
|
||||
chatEl.scrollTop = chatEl.scrollHeight;
|
||||
abort = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstChunk) {
|
||||
bubble.classList.remove('loader');
|
||||
bubble.innerHTML = '';
|
||||
firstChunk = false;
|
||||
document.getElementById('ai-cloud')?.classList.add('d-none');
|
||||
}
|
||||
|
||||
// Text sammeln und verzögert rendern
|
||||
raw += text;
|
||||
scheduleRender();
|
||||
});
|
||||
}
|
||||
|
||||
} catch {
|
||||
bubble.innerHTML += '<br><em>Error occurred.</em>';
|
||||
} finally {
|
||||
if (renderTimer) clearTimeout(renderTimer);
|
||||
sendBtn.disabled = false;
|
||||
abortBtn.disabled = true;
|
||||
clearBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
abortBtn.addEventListener('click', () => {
|
||||
abort = true;
|
||||
addMessage('assistant', '<em>[aborted]</em>');
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', async () => {
|
||||
await fetch('/history/delete', { method: 'POST' });
|
||||
chatEl.innerHTML = '';
|
||||
addMessage('assistant', '<em>History cleared.</em>');
|
||||
});
|
||||
});
|
||||
7
public/assets/js/bootstrap.bundle.min.js
vendored
Normal file
7
public/assets/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
69
public/assets/js/marked.min.js
vendored
Normal file
69
public/assets/js/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
public/assets/js/purify.min.js
vendored
Normal file
3
public/assets/js/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
236
public/assets/styles/base.css
Normal file
236
public/assets/styles/base.css
Normal file
@@ -0,0 +1,236 @@
|
||||
:root {
|
||||
--bg: #0f172a;
|
||||
--panel: #020617;
|
||||
--border: #334155;
|
||||
--text: #d3d6dc;
|
||||
--muted: #94a3b8;
|
||||
--accent: #2563eb;
|
||||
--user: #1e40af;
|
||||
--assistant: #020617;
|
||||
--danger: #dc2626;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.49rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.31rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.13rem;
|
||||
}
|
||||
|
||||
h4, h5, h6 {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
form, input, button, textarea {
|
||||
font-size: 0.85rem !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 4rem);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.header .spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chat {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message.user .bubble {
|
||||
background: var(--user);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.message.assistant .bubble {
|
||||
background: var(--assistant);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 80%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 6px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.bubble.loader {
|
||||
font-style: italic;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Markdown styling */
|
||||
.bubble p {
|
||||
margin: 0.4rem 0;
|
||||
}
|
||||
|
||||
.bubble ul {
|
||||
padding-left: 1.2rem;
|
||||
}
|
||||
|
||||
.bubble code {
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
padding: 0.15rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, monospace;
|
||||
}
|
||||
|
||||
.bubble pre {
|
||||
background: #020617;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.bubble h1,
|
||||
.bubble h2,
|
||||
.bubble h3,
|
||||
.bubble h4,
|
||||
.bubble h5,
|
||||
.bubble h6{
|
||||
margin-top: .5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.bubble table {
|
||||
width: 99%
|
||||
}
|
||||
.bubble td {
|
||||
border-top: 1px dotted;
|
||||
padding: .5rem .5rem .5rem .0rem;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
textarea{
|
||||
color: var(--muted) !important;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--muted);
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
textarea::placeholder {
|
||||
color: var(--muted) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
textarea::-webkit-input-placeholder {
|
||||
color: var(--muted) !important;
|
||||
}
|
||||
|
||||
textarea::-moz-placeholder {
|
||||
color: var(--muted) !important;
|
||||
}
|
||||
|
||||
textarea:-ms-input-placeholder {
|
||||
color: var(--muted) !important;
|
||||
}
|
||||
|
||||
textarea:-moz-placeholder {
|
||||
color: var(--muted) !important;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bg-dark {
|
||||
background-color: var(--assistant) !important;
|
||||
color: #fff;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-trans {
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-trans:disabled {
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
opacity: .4 !important;
|
||||
}
|
||||
|
||||
.ai-cloud {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 30% 30%, rgba(37, 99, 235, 0.4), transparent 70%),
|
||||
radial-gradient(circle at 70% 70%, rgba(148, 163, 184, 0.3), transparent 80%),
|
||||
radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.1), transparent 100%);
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
filter: blur(30px);
|
||||
animation: cloud-move 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes cloud-move {
|
||||
0%, 100% {
|
||||
transform: translateX(-50%) scale(.5);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.chat {
|
||||
position: relative;
|
||||
/* ... vorhandene Styles ... */
|
||||
}
|
||||
|
||||
#ai-cloud{
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: absolute;
|
||||
}
|
||||
6
public/assets/styles/bootstrap.min.css
vendored
Normal file
6
public/assets/styles/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user