Parse assistant messages as Markdown (marked.js)

This commit is contained in:
Meleki committed 2026-02-27 14:47:33 +01:00
1 parent 6556d62f17
commit 18b2feef4a
7 files changed
+157 -45

No files matched your search

-17
View File
@@ -1,17 +0,0 @@
# Gott – Zusammenfassung
Self-hosted AI-Coding-Agent auf Basis der Anthropic API.
Web-UI für Chat-Sessions mit konfigurierbaren Agenten, die Tool-Zugriff (Dateien lesen/schreiben/bearbeiten, Shell-Befehle ausführen) auf definierte Workspace-Verzeichnisse haben.
## Stack
- .NET 10, SQLite, Vanilla-Frontend
- Port 19000
## Struktur
- **Endpoints/** – API-Routen (Chat, Sessions, Agents, History, Workspace)
- **Services/** – Anthropic-API-Anbindung, Tool-Ausführung, Datenbankzugriff (SQLite)
- **Models/** – Datenmodelle (Sessions, Messages, Agents, Chains, Archives)
- **wwwroot/** – Frontend (statische Dateien)
+3 -22
View File
@@ -1,27 +1,8 @@
#!/bin/bash
set -e
_hostname=$(hostname)
if [ "$_hostname" = "stein2" ]; then
cd /home/node/.openclaw/workspace/projects/gott
systemctl stop gott
elif [ "$_hostname" = "meleki-container" ]; then
cd ~/.openclaw/workspace/projects/gott
else
echo "$_hostname: deployment not allowed"
exit
fi
cd /app/gott
export PATH="$HOME/.dotnet:$PATH"
systemctl stop gott
dotnet publish -c Release -o publish
if [ "$_hostname" = "stein2" ]; then
systemctl start gott
elif [ "$_hostname" = "meleki-container" ]; then
ssh -o ConnectTimeout=5 meleki@192.168.100.200 "nohup sudo systemctl restart gott > /dev/null 2>&1 &"
fi
systemctl start gott
echo "OK"
+125
View File
@@ -0,0 +1,125 @@
/* Markdown in Chat Bubbles */
.msg.markdown {
white-space: normal;
}
.msg.markdown > *:first-child {
margin-top: 0;
}
.msg.markdown > *:last-child {
margin-bottom: 0;
}
.msg.markdown p {
margin: 0 0 var(--s3) 0;
}
.msg.markdown p:last-child {
margin-bottom: 0;
}
.msg.markdown h1,
.msg.markdown h2,
.msg.markdown h3,
.msg.markdown h4,
.msg.markdown h5,
.msg.markdown h6 {
margin: var(--s4) 0 var(--s2) 0;
}
.msg.markdown h1:first-child,
.msg.markdown h2:first-child,
.msg.markdown h3:first-child {
margin-top: 0;
}
.msg.markdown ul,
.msg.markdown ol {
margin: 0 0 var(--s3) 0;
padding-left: 1.5em;
}
.msg.markdown li {
margin-bottom: var(--s1);
}
.msg.markdown li:last-child {
margin-bottom: 0;
}
.msg.markdown code {
background: var(--f3-bg);
padding: .1em .3em;
font-size: .9em;
}
.msg.markdown pre {
background: var(--f3-bg);
border: var(--border-width) solid var(--f3-border);
padding: var(--s3);
margin: 0 0 var(--s3) 0;
overflow-x: auto;
}
.msg.markdown pre:last-child {
margin-bottom: 0;
}
.msg.markdown pre code {
background: none;
padding: 0;
font-size: .9em;
}
.msg.markdown blockquote {
border-left: 3px solid var(--f3-border);
margin: 0 0 var(--s3) 0;
padding: var(--s2) var(--s3);
opacity: var(--semi-transparent);
}
.msg.markdown blockquote:last-child {
margin-bottom: 0;
}
.msg.markdown hr {
border: none;
border-top: var(--border-width) solid var(--f3-border);
margin: var(--s3) 0;
}
.msg.markdown table {
border-collapse: collapse;
margin: 0 0 var(--s3) 0;
width: 100%;
}
.msg.markdown table:last-child {
margin-bottom: 0;
}
.msg.markdown th,
.msg.markdown td {
border: var(--border-width) solid var(--f3-border);
padding: var(--s2) var(--s3);
text-align: left;
}
.msg.markdown th {
background: var(--f3-bg);
}
.msg.markdown a {
color: var(--primary);
text-decoration: underline;
}
.msg.markdown img {
max-width: 100%;
}
.msg.markdown input[type="checkbox"] {
margin-right: var(--s2);
}
+2
View File
@@ -14,6 +14,7 @@
<link rel="stylesheet" href="css/chat.css">
<link rel="stylesheet" href="css/placeholder.css">
<link rel="stylesheet" href="css/file-explorer.css">
<link rel="stylesheet" href="css/markdown.css">
</head>
<body>
@@ -162,6 +163,7 @@
</div>
</template>
<script src="js/lib/marked.min.js"></script>
<script src="js/ui.js"></script>
<script src="js/api.js"></script>
<script src="js/session.js"></script>
+7 -2
View File
@@ -109,8 +109,13 @@ async function loadLatestArchive() {
const div = document.createElement('div');
switch (msg.type) {
case 'text':
div.className = msg.role === 'user' ? 'msg user archived' : 'msg assistant archived';
div.textContent = (msg.content || '').trim();
if (msg.role === 'user') {
div.className = 'msg user archived';
div.textContent = (msg.content || '').trim();
} else {
div.className = 'msg assistant markdown archived';
div.innerHTML = renderMarkdown(msg.content);
}
if (msg.role === 'user' && msg.attachmentCount > 0) {
let names = [];
try { if (msg.attachmentNames) names = JSON.parse(msg.attachmentNames); } catch {}
+6
View File
@@ -0,0 +1,6 @@
/**
* marked v15.0.7 - a markdown parser
* Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed)
* https://github.com/markedjs/marked
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s={exec:()=>null};function r(e,t=""){let n="string"==typeof e?e:e.source;const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(i.caret,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}const i={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^<a /i,endATag:/^<\/a>/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^</,endAngleBracket:/>$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:e=>new RegExp(`^( {0,3}${e})((?:[\t ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),hrRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}#`),htmlBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}<(?:[a-z].*>|!--)`,"i")},l=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,o=/(?:[*+-]|\d{1,9}[.)])/,a=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,c=r(a).replace(/bull/g,o).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),h=r(a).replace(/bull/g,o).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),p=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,u=/(?!\s*\])(?:\\.|[^\[\]\\])+/,g=r(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",u).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),k=r(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,o).getRegex(),d="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",f=/<!--(?:-?>|[\s\S]*?(?:-->|$))/,x=r("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$))","i").replace("comment",f).replace("tag",d).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegexLine truncated
+14 -4
View File
@@ -4,6 +4,15 @@ const $ = (sel) => document.querySelector(sel);
const chatEl = () => $('#chat');
const metaEl = () => $('#meta');
// ── Markdown ──
marked.setOptions({ breaks: true, gfm: true });
function renderMarkdown(text) {
if (!text) return '';
return marked.parse(text.trim());
}
function scrollDown() {
const chat = chatEl();
chat.scrollTop = chat.scrollHeight;
@@ -32,8 +41,8 @@ function addUserBubble(text, streamed = false) {
function addAssistantBubble(text) {
const div = document.createElement('div');
div.className = 'msg assistant';
div.textContent = (text || '').trim();
div.className = 'msg assistant markdown';
div.innerHTML = renderMarkdown(text);
chatEl().appendChild(div);
scrollDown();
return div;
@@ -56,7 +65,7 @@ let _streamText = '';
function startStreamBubble() {
_streamDiv = document.createElement('div');
_streamDiv.className = 'msg assistant streamed';
_streamDiv.className = 'msg assistant markdown streamed';
_streamDiv.innerHTML = '<span class="cursor"></span>';
_streamText = '';
chatEl().appendChild(_streamDiv);
@@ -66,7 +75,7 @@ function startStreamBubble() {
function appendToStream(text) {
if (!_streamDiv) startStreamBubble();
_streamText += text;
_streamDiv.textContent = _streamText;
_streamDiv.innerHTML = renderMarkdown(_streamText);
const cursor = document.createElement('span');
cursor.className = 'cursor';
_streamDiv.appendChild(cursor);
@@ -78,6 +87,7 @@ function endStreamBubble() {
const cursor = _streamDiv.querySelector('.cursor');
if (cursor) cursor.remove();
if (!_streamText.trim()) _streamDiv.remove();
else _streamDiv.innerHTML = renderMarkdown(_streamText);
}
_streamDiv = null;
_streamText = '';