chat design

This commit is contained in:
Bastian Wagner
2026-06-12 14:39:49 +02:00
parent d75c0ecc68
commit a921095f3a
3 changed files with 131 additions and 4 deletions

View File

@@ -10,7 +10,10 @@
<div class="message-list" aria-live="polite"> <div class="message-list" aria-live="polite">
@for (message of messages(); track $index) { @for (message of messages(); track $index) {
<article class="message" [class.user-message]="message.role === 'user'"> <article class="message" [class.user-message]="message.role === 'user'">
<p>{{ message.content }}</p> <div
class="message-content"
[innerHTML]="formatMessage(message.content)"
></div>
@if (message.actions?.length) { @if (message.actions?.length) {
<div class="action-list"> <div class="action-list">

View File

@@ -68,10 +68,45 @@
color: var(--mat-sys-on-surface); color: var(--mat-sys-on-surface);
} }
.message p { .message-content {
margin: 0; display: grid;
gap: 0.5rem;
overflow-wrap: anywhere; overflow-wrap: anywhere;
line-height: 1.4; line-height: 1.45;
}
.message-content :where(p, h3, h4, h5, ol, ul) {
margin: 0;
}
.message-content :where(h3, h4, h5) {
font-size: 0.98rem;
font-weight: 600;
}
.message-content :where(ol, ul) {
display: grid;
gap: 0.35rem;
padding-left: 1.25rem;
}
.message-content li {
overflow-wrap: anywhere;
}
.message-content hr {
width: 100%;
margin: 0.15rem 0;
border: 0;
border-top: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 75%, transparent);
}
.message-content code {
padding: 0.08rem 0.25rem;
border-radius: 4px;
background: color-mix(in srgb, var(--mat-sys-outline-variant) 22%, transparent);
font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace;
font-size: 0.9em;
} }
.action-list { .action-list {

View File

@@ -103,4 +103,93 @@ export class AssistantChatComponent {
return `Item hinzugefuegt: ${action.itemTitle}`; return `Item hinzugefuegt: ${action.itemTitle}`;
} }
protected formatMessage(content: string): string {
const lines = this.escapeHtml(content).split(/\r?\n/);
const html: string[] = [];
let listMode: 'ol' | 'ul' | null = null;
const closeList = () => {
if (!listMode) {
return;
}
html.push(`</${listMode}>`);
listMode = null;
};
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line) {
closeList();
continue;
}
if (/^-{3,}$/.test(line)) {
closeList();
html.push('<hr>');
continue;
}
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
if (headingMatch) {
closeList();
const level = headingMatch[1].length + 2;
html.push(
`<h${level}>${this.formatInlineMarkdown(headingMatch[2])}</h${level}>`,
);
continue;
}
const orderedMatch = line.match(/^\d+\.\s+(.+)$/);
if (orderedMatch) {
if (listMode !== 'ol') {
closeList();
html.push('<ol>');
listMode = 'ol';
}
html.push(`<li>${this.formatInlineMarkdown(orderedMatch[1])}</li>`);
continue;
}
const unorderedMatch = line.match(/^[-*]\s+(.+)$/);
if (unorderedMatch) {
if (listMode !== 'ul') {
closeList();
html.push('<ul>');
listMode = 'ul';
}
html.push(`<li>${this.formatInlineMarkdown(unorderedMatch[1])}</li>`);
continue;
}
closeList();
html.push(`<p>${this.formatInlineMarkdown(line)}</p>`);
}
closeList();
return html.join('');
}
private formatInlineMarkdown(value: string): string {
return value
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`(.+?)`/g, '<code>$1</code>');
}
private escapeHtml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
} }