feat: Timestamps + LaTeX/KaTeX-Rendering im Chat
Timestamps (dd.MM.yyyy HH:mm:ss): - Neuer _formatTimestamp() + _addTimestamp() Helper in ui.js - Timestamp bei allen User- und Assistant-Nachrichten (live, history, archiv) - Erscheint on hover, halbtransparent, rechts oben an der Bubble LaTeX/Math-Rendering mit KaTeX 0.16.21: - KaTeX lokal eingebunden (JS + CSS + 21 woff2-Fonts) - renderMarkdown() schützt LaTeX vor Markdown-Parsing - $$...$$ → Block-Math (zentriert), $...$ → Inline-Math - Fehlerhafte Ausdrücke als <code> Fallback - KaTeX-Display Styling in markdown.css Weitere Änderungen: - theme.css: Neue --info Farbe - Blockquote-Styling: info-Farbe statt halbtransparent
This commit is contained in:
1 parent
dbeba6535a
commit
489b5e0eb0
29 files changed
+119
-11
No files matched your search
@@ -186,6 +186,7 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds) {
|
||||
div.dataset.timestamp = msg.createdAt || '';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
renderUserContent(div, msg.content);
|
||||
_addTimestamp(div, msg.createdAt);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, msg.id, msg.deletedOn);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
@@ -215,6 +216,7 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds) {
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
div.innerHTML = renderMarkdown(msg.content);
|
||||
parseFluentEmojis(div);
|
||||
_addTimestamp(div, msg.createdAt);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, msg.id, msg.deletedOn);
|
||||
_addMarkButton(div, msg.id, msg.isMarked);
|
||||
@@ -346,10 +348,12 @@ async function loadLatestArchive() {
|
||||
div.className = 'msg user markdown archived';
|
||||
if (msg.id) div.dataset.messageId = msg.id;
|
||||
renderUserContent(div, msg.content);
|
||||
_addTimestamp(div, msg.createdAt);
|
||||
} else {
|
||||
div.className = 'msg assistant markdown archived';
|
||||
div.innerHTML = renderMarkdown(msg.content);
|
||||
parseFluentEmojis(div);
|
||||
_addTimestamp(div, msg.createdAt);
|
||||
}
|
||||
if (msg.role === 'user' && msg.attachmentCount > 0) {
|
||||
if (/\[Bild: [^\]]+\]/.test(msg.content)) archiveImageIds.push(msg.id);
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.katex=t():e.katex=t()}("undefined"!=typeof self?self:this,(function(){return function(){"use strict";var e={d:function(t,r){for(var n in r)e.o(r,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},o:function(e,t){return Object.prototype.hasOwnProperty.call(e,t)}},t={};e.d(t,{default:function(){return Wn}});class r{constructor(e,t){this.name=void 0,this.position=void 0,this.length=void 0,this.rawMessage=void 0;let n,o,s="KaTeX parse error: "+e;const i=t&&t.loc;if(i&&i.start<=i.end){const e=i.lexer.input;n=i.start,o=i.end,n===e.length?s+=" at end of input: ":s+=" at position "+(n+1)+": ";const t=e.slice(n,o).replace(/[^]/g,"$&\u0332");let r,a;r=n>15?"\u2026"+e.slice(n-15,n):e.slice(0,n),a=o+15<e.length?e.slice(o,o+15)+"\u2026":e.slice(o),s+=r+t+a}const a=new Error(s);return a.name="ParseError",a.__proto__=r.prototype,a.position=n,null!=n&&null!=o&&(a.length=o-n),a.rawMessage=e,a}}r.prototype.__proto__=Error.prototype;var n=r;const o=/([A-Z])/g,s={"&":"&",">":">","<":"<",'"':""","'":"'"},i=/[&><"']/g;const a=function(e){return"ordgroup"===e.type||"color"===e.type?1===e.body.length?a(e.body[0]):e:"font"===e.type?a(e.body):e};var l={contains:function(e,t){return-1!==e.indexOf(t)},deflt:function(e,t){return void 0===e?t:e},escape:function(e){return String(e).replace(i,(e=>s[e]))},hyphenate:function(e){return e.replace(o,"-$1").toLowerCase()},getBaseElem:a,isCharacterBox:function(e){const t=a(e);return"mathord"===t.type||"textord"===t.type||"atom"===t.type},protocolFromUrl:function(e){const t=/^[\x00-\x20]*([^\\/#?]*?)(:|�*58|�*3a|&colon)/i.exec(e);return t?":"!==t[2]?null:/^[a-zA-Z][a-zA-Z0-9+\-.]*$/.test(t[1])?t[1].toLowerCase():null:"_relative"}};const h={displayMode:{type:"boolean",description:"Render math in display mode, which puts the math in display style (so \\int and \\sum are large, for example), and centers the math on the page on its own line.",cli:"-d, --display-mode"},output:{type:{enum:["htmlAndMathml","html","mathml"]},description:"Determines the markup language of the output.",cli:"-F, --format <type>"},leqno:{type:"boolean",description:"Render display math in leqno style (left-justified tags)."},fleqn:{type:"boolean",description:"Render display math flush left."},throwOnError:{type:"boolean",default:!0,cli:"-t, --no-throw-on-error",cliDescription:"Render errors (in the color given by --error-color) instead of throwing a ParseError exception when encountering an error."},errorColor:{type:"string",default:"#cc0000",cli:"-c, --error-color <color>",cliDescription:"A color string given in the format 'rgb' or 'rrggbb' (no #). This option determines the color of errors rendered by the -t option.",cliProcessor:e=>"#"+e},macros:{type:"object",cli:"-m, --macro <def>",cliDescription:"Define custom macro of the form '\\foo:expansion' (use multiple -m arguments for multiple macros).",cliDefault:[],cliProcessor:(e,t)=>(t.push(e),t)},minRuleThickness:{type:"number",description:"Specifies a minimum thickness, in ems, for fraction lines, `\\sqrt` top lines, `{array}` vertical lines, `\\hline`, `\\hdashline`, `\\underline`, `\\overline`, and the borders of `\\fbox`, `\\boxed`, and `\\fcolorbox`.",processor:e=>Math.max(0,e),cli:"--min-rule-thickness <size>",cliProcessor:parseFloat},colorIsTextColor:{type:"boolean",description:"Makes \\color behave like LaTeX's 2-argument \\textcolor, instead of LaTeX's one-argument \\color mode change.",cli:"-b, --color-is-text-color"},strict:{type:[{enum:["warn","ignore","error"]},"boolean","function"],description:"Turn on strict / LaTeX faithfulness mode, which throws an error if the input uses features that are not supported by LaTeX.",cli:"-S, --strict",cliDefault:!1},trust:{type:["boolean","function"],description:"Trust the input, enabling all HTML features such as \\url.",cli:"-T, --trust"},maxSize:{type:"number",default:1/0,description:"If non-zero, all user-specified sizes, e.g. in \\rule{500em}{500em}, will be capped to maxSize ems. Otherwise, elements and spaces can be arbitrarily large",processor:e=>Math.max(0,e),cli:"-s, --max-size <n>",cliProcessor:parseInt},maxExpand:{type:"number",default:1e3,description:"Limit the number of macro expansions to the specified number, to prevent e.g. infinite macro loops. If set to Infinity, the macro expander will try to fully expand as in LaTeX.",processor:e=>Math.max(0,e),cli:"-e, --max-expand <n>",cliProcessor:e=>"Infinity"===e?1/0:parseInt(e)},globalGroup:{type:"boolean",cli:!1}};function c(e){if(e.default)return e.default;const t=e.type,r=Array.isArray(t)?t[0]:t;if("string"!=typeof r)return r.enum[0];switch(r){case"boolean":return!1;case"string":return"";case"number":return 0;case"object":return{}}}class m{constructor(e){this.displayMode=void 0,this.output=void 0,this.leqno=void 0,this.fleqn=void 0,this.throwOnError=void 0Line truncated
|
||||
+69
-5
@@ -54,14 +54,33 @@ marked.setOptions({ breaks: true, gfm: true });
|
||||
|
||||
function renderMarkdown(text) {
|
||||
if (!text) return '';
|
||||
var html = marked.parse(text.trim());
|
||||
// Code-Blöcke (<pre>...<code>...</code>...</pre> und inline <code>...</code>) schützen
|
||||
var src = text.trim();
|
||||
|
||||
// 1) LaTeX-Ausdrücke VOR dem Markdown-Parsing schützen
|
||||
// Reihenfolge: zuerst Block ($$...$$), dann Inline ($...$)
|
||||
var mathBlocks = [];
|
||||
// Block-Math: $$...$$ (auch mehrzeilig)
|
||||
src = src.replace(/\$\$([\s\S]+?)\$\$/g, function(m, latex) {
|
||||
mathBlocks.push({ latex: latex, displayMode: true });
|
||||
return '%%MATH' + (mathBlocks.length - 1) + '%%';
|
||||
});
|
||||
// Inline-Math: $...$ (nicht-gierig, kein $ am Anfang/Ende)
|
||||
src = src.replace(/(?<!\$)\$(?!\$)(.+?)(?<!\$)\$(?!\$)/g, function(m, latex) {
|
||||
mathBlocks.push({ latex: latex, displayMode: false });
|
||||
return '%%MATH' + (mathBlocks.length - 1) + '%%';
|
||||
});
|
||||
|
||||
// 2) Markdown parsen
|
||||
var html = marked.parse(src);
|
||||
|
||||
// 3) Code-Blöcke schützen (damit Emojis/Krea dort nicht greifen)
|
||||
var codeBlocks = [];
|
||||
html = html.replace(/<pre[\s>][\s\S]*?<\/pre>|<code[\s>][\s\S]*?<\/code>/gi, function(m) {
|
||||
codeBlocks.push(m);
|
||||
return '%%CODE' + (codeBlocks.length - 1) + '%%';
|
||||
});
|
||||
// [Krea: dateiname.png] → Inline-Thumbnail mit Klick für Vollbild
|
||||
|
||||
// 4) [Krea: dateiname.png] → Inline-Thumbnail mit Klick für Vollbild
|
||||
html = html.replace(/\[Krea:\s*([^\]]+)\]/g, function(match, filename) {
|
||||
var fn = filename.trim();
|
||||
var safeFn = esc(fn);
|
||||
@@ -71,12 +90,30 @@ function renderMarkdown(text) {
|
||||
+ '<img class="krea-thumb" src="' + thumbUrl + '" alt="' + safeFn + '" loading="lazy">'
|
||||
+ '</span>';
|
||||
});
|
||||
// Unicode-Emojis durch Fluent Emoji 3D ersetzen (außerhalb von Code-Blöcken)
|
||||
|
||||
// 5) Unicode-Emojis durch Fluent Emoji 3D ersetzen (außerhalb von Code-Blöcken)
|
||||
html = replaceEmojisWithFluent(html);
|
||||
// Code-Blöcke wiederherstellen
|
||||
|
||||
// 6) Code-Blöcke wiederherstellen
|
||||
html = html.replace(/%%CODE(\d+)%%/g, function(m, idx) {
|
||||
return codeBlocks[parseInt(idx)];
|
||||
});
|
||||
|
||||
// 7) LaTeX-Platzhalter durch KaTeX-gerenderten HTML ersetzen
|
||||
html = html.replace(/%%MATH(\d+)%%/g, function(m, idx) {
|
||||
var block = mathBlocks[parseInt(idx)];
|
||||
try {
|
||||
return katex.renderToString(block.latex.trim(), {
|
||||
displayMode: block.displayMode,
|
||||
throwOnError: false,
|
||||
output: 'html'
|
||||
});
|
||||
} catch (e) {
|
||||
// Fallback: Originaltext anzeigen
|
||||
return '<code class="katex-error">' + esc(block.latex) + '</code>';
|
||||
}
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -307,6 +344,31 @@ function _showShareMenu(anchorBtn) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Timestamp ──
|
||||
|
||||
function _formatTimestamp(isoString) {
|
||||
if (!isoString) return '';
|
||||
var d = new Date(isoString);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
var dd = String(d.getDate()).padStart(2, '0');
|
||||
var mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
var yyyy = d.getFullYear();
|
||||
var hh = String(d.getHours()).padStart(2, '0');
|
||||
var min = String(d.getMinutes()).padStart(2, '0');
|
||||
var ss = String(d.getSeconds()).padStart(2, '0');
|
||||
return dd + '.' + mm + '.' + yyyy + ' ' + hh + ':' + min + ':' + ss;
|
||||
}
|
||||
|
||||
function _addTimestamp(div, timestamp) {
|
||||
if (!timestamp) return;
|
||||
var formatted = _formatTimestamp(timestamp);
|
||||
if (!formatted) return;
|
||||
var span = document.createElement('span');
|
||||
span.className = 'msg-timestamp';
|
||||
span.textContent = formatted;
|
||||
div.prepend(span);
|
||||
}
|
||||
|
||||
// ── Reply Button ──
|
||||
|
||||
function _addReplyButton(div) {
|
||||
@@ -440,6 +502,7 @@ function addUserBubble(text, streamed = false, timestamp = '', messageId = null,
|
||||
if (messageId) div.dataset.messageId = messageId;
|
||||
if (streamed) div.classList.add('streamed');
|
||||
renderUserContent(div, text, inlineImages);
|
||||
_addTimestamp(div, timestamp);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, messageId, deletedOn);
|
||||
_addMarkButton(div, messageId, isMarked);
|
||||
@@ -458,6 +521,7 @@ function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = f
|
||||
div.innerHTML = renderMarkdown(text);
|
||||
parseFluentEmojis(div);
|
||||
wrapKreaGalleries(div);
|
||||
_addTimestamp(div, timestamp);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, messageId, deletedOn);
|
||||
_addMarkButton(div, messageId, isMarked);
|
||||
|
||||
Reference in new issue
Block a user