Der Mini-Renderer kannte weder Tabellen noch ---, also landeten beide als roher Text in einem Absatz: aus einer Anforderung/Erfahrung-Tabelle wurde eine einzeilige Pipe-Wueste, aus --- blieb ---. Das Modell nutzt aber gerade fuer Passungs-Vergleiche gern Tabellen. - GFM-Tabellen inkl. Ausrichtung (:--:), auch ohne Rand-Pipes. - Krumme Zeilen werden auf die Kopfbreite gezogen (fehlende Zellen leer, ueberzaehlige raus), sonst zerreisst eine Zeile das Raster. - Wrapper scrollt horizontal, damit eine breite Tabelle die Chat-Blase auf dem Handy nicht auseinanderzieht. - Zellinhalte laufen durch dieselbe Escape-/inlineFmt-Kette wie der Rest, HTML aus dem Modell kann also weiterhin nichts injizieren. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
9.0 KiB
Plaintext
141 lines
9.0 KiB
Plaintext
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<%- include('partials/head') %>
|
|
<style>
|
|
#chatLog::-webkit-scrollbar { width: 8px; }
|
|
#chatLog::-webkit-scrollbar-thumb { background: rgba(120,120,120,.4); border-radius: 4px; }
|
|
/* Typing indicator: three dots with a staggered bounce. */
|
|
.typing-dot {
|
|
width: 7px; height: 7px; border-radius: 9999px;
|
|
background: currentColor; display: inline-block;
|
|
opacity: 0.35;
|
|
animation: chat-typing 1.2s infinite ease-in-out both;
|
|
}
|
|
.typing-dot:nth-child(2) { animation-delay: 0.18s; }
|
|
.typing-dot:nth-child(3) { animation-delay: 0.36s; }
|
|
@keyframes chat-typing {
|
|
0%, 80%, 100% { transform: translateY(0); opacity: 0.35; }
|
|
40% { transform: translateY(-5px); opacity: 1; }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.typing-dot { animation: none; opacity: 0.6; }
|
|
}
|
|
/* Thread rename/delete rely on hover/focus to appear — neither exists on a
|
|
touchscreen, so the controls would be unreachable. Show them on small
|
|
(likely touch) screens. */
|
|
@media (max-width: 768px) {
|
|
.threadRename, .threadDelete { opacity: 1 !important; }
|
|
}
|
|
/* Markdown rendering inside assistant bubbles */
|
|
.chat-md { line-height: 1.5; }
|
|
.chat-md > *:first-child { margin-top: 0; }
|
|
.chat-md > *:last-child { margin-bottom: 0; }
|
|
.chat-md p { margin: 0 0 0.5rem 0; }
|
|
.chat-md h1, .chat-md h2, .chat-md h3 { font-weight: 600; margin: 0.6rem 0 0.3rem 0; line-height: 1.25; }
|
|
.chat-md h1 { font-size: 1.05rem; }
|
|
.chat-md h2 { font-size: 1rem; }
|
|
.chat-md h3 { font-size: 0.95rem; }
|
|
.chat-md ul, .chat-md ol { margin: 0 0 0.5rem 0; padding-left: 1.25rem; }
|
|
.chat-md li { margin: 0.15rem 0; }
|
|
.chat-md code { background: rgba(0,0,0,0.08); padding: 0.1rem 0.3rem; border-radius: 4px; font-size: 0.85em; }
|
|
.dark .chat-md code { background: rgba(255,255,255,0.12); }
|
|
.chat-md pre { background: rgba(0,0,0,0.08); padding: 0.6rem; border-radius: 6px; overflow-x: auto; margin: 0 0 0.5rem 0; }
|
|
.dark .chat-md pre { background: rgba(0,0,0,0.35); }
|
|
.chat-md pre code { background: none; padding: 0; }
|
|
.chat-md a { text-decoration: underline; }
|
|
.chat-md hr { border: 0; border-top: 1px solid rgba(0,0,0,0.12); margin: 0.7rem 0; }
|
|
.dark .chat-md hr { border-top-color: rgba(255,255,255,0.15); }
|
|
/* Tabellen (z. B. Anforderung/Erfahrung-Vergleiche). Der Wrapper scrollt,
|
|
damit eine breite Tabelle nicht die Chat-Blase auseinanderzieht. */
|
|
.chat-md-table { overflow-x: auto; margin: 0 0 0.5rem 0; }
|
|
.chat-md table { border-collapse: collapse; width: 100%; font-size: 0.875rem; }
|
|
.chat-md th, .chat-md td { border: 1px solid rgba(0,0,0,0.12); padding: 0.35rem 0.55rem; text-align: left; vertical-align: top; }
|
|
.chat-md th { background: rgba(0,0,0,0.04); font-weight: 600; }
|
|
.dark .chat-md th, .dark .chat-md td { border-color: rgba(255,255,255,0.15); }
|
|
.dark .chat-md th { background: rgba(255,255,255,0.06); }
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen flex flex-col transition-colors duration-300 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100" id="body">
|
|
<%- include('partials/header', { hideSettings: true }) %>
|
|
|
|
<main class="container mx-auto px-4 py-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h2 class="text-xl font-bold text-gray-800 dark:text-white">KI-Bewerbungs-Assistent</h2>
|
|
<button id="newThreadBtn"
|
|
class="px-3 py-1.5 rounded-md bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium">
|
|
+ Neuer Chat
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-[240px_1fr] gap-3 min-h-[360px] md:h-[calc(100vh-180px)]">
|
|
<!-- Thread sidebar -->
|
|
<aside class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden flex flex-col max-h-48 md:max-h-none">
|
|
<div class="px-3 py-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">Verläufe</div>
|
|
<ul id="threadList" class="overflow-y-auto flex-1 divide-y divide-gray-100 dark:divide-gray-700">
|
|
<% threads.forEach(function(t) { %>
|
|
<li data-thread-li="<%= t.id %>">
|
|
<div class="flex items-center group <%= (t.id === activeId) ? 'bg-blue-50 dark:bg-gray-700' : '' %>">
|
|
<a href="/chat?thread=<%= t.id %>"
|
|
data-thread="<%= t.id %>"
|
|
class="threadLink flex-1 min-w-0 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700/40">
|
|
<div class="truncate font-medium"><%= t.titel || '(ohne Titel)' %></div>
|
|
<div class="text-[11px] text-gray-400"><%= new Date(t.updated_at).toLocaleDateString('de-DE') %></div>
|
|
</a>
|
|
<button type="button" data-rename-thread="<%= t.id %>"
|
|
class="threadRename shrink-0 p-1.5 rounded text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30 opacity-0 group-hover:opacity-100 focus:opacity-100"
|
|
title="Namen ändern" aria-label="Namen ändern">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
</svg>
|
|
</button>
|
|
<button type="button" data-delete-thread="<%= t.id %>"
|
|
class="threadDelete shrink-0 mr-2 p-1.5 rounded text-gray-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 opacity-0 group-hover:opacity-100 focus:opacity-100"
|
|
title="Verlauf löschen" aria-label="Verlauf löschen">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M1 7h22M9 7V4a1 1 0 011-1h4a1 1 0 011 1v3"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</li>
|
|
<% }); %>
|
|
<% if (!threads.length) { %>
|
|
<li class="px-3 py-4 text-sm text-gray-400 text-center">Noch keine Verläufe.</li>
|
|
<% } %>
|
|
</ul>
|
|
</aside>
|
|
|
|
<!-- Message area -->
|
|
<section class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 flex flex-col overflow-hidden h-[60vh] md:h-auto">
|
|
<div id="chatLog" class="flex-1 overflow-y-auto p-4 space-y-3 text-gray-800 dark:text-gray-100">
|
|
<% if (!messages.length) { %>
|
|
<div class="h-full flex items-center justify-center text-gray-400 text-sm px-6">
|
|
Stelle eine Frage zu deinen Bewerbungen.
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
|
|
<form id="chatForm" class="border-t border-gray-200 dark:border-gray-700 p-3 flex gap-2">
|
|
<textarea id="chatInput" rows="1" autocomplete="off"
|
|
class="flex-1 resize-none rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-3 py-2 text-sm text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="Frage stellen … (Enter zum Senden, Shift+Enter = Zeilenumbruch)"></textarea>
|
|
<button type="submit"
|
|
class="px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium disabled:opacity-50"
|
|
id="sendBtn">Senden</button>
|
|
</form>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
|
|
<script id="initialMessages" type="application/json"><%- JSON.stringify(messages.map(function(m){ return { role: m.role, content: m.content }; })).replace(/</g, '\\u003c') %></script>
|
|
<input type="hidden" id="activeThread" value="<%= activeId || '' %>">
|
|
|
|
<script>
|
|
window.__CHAT_BOOT__ = {
|
|
activeId: <%= activeId ? JSON.stringify(activeId) : 'null' %>,
|
|
needsThread: <%= activeId ? 'false' : 'true' %>
|
|
};
|
|
</script>
|
|
<script src="/js/chat.js"></script>
|
|
</body>
|
|
</html> |