diff --git a/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx b/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx
index 3e5c6affdf..10e76bcae4 100644
--- a/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx
+++ b/apps/client/src/widgets/type_widgets/llm_chat/ChatMessage.tsx
@@ -1,7 +1,15 @@
+import { useMemo } from "preact/hooks";
+import { marked } from "marked";
import { t } from "../../../services/i18n.js";
import type { Citation } from "../../../services/llm_chat.js";
import "./LlmChat.css";
+// Configure marked for safe rendering
+marked.setOptions({
+ breaks: true, // Convert \n to
+ gfm: true // GitHub Flavored Markdown
+});
+
interface StoredMessage {
id: string;
role: "user" | "assistant" | "system";
@@ -18,14 +26,31 @@ interface Props {
export default function ChatMessage({ message, isStreaming }: Props) {
const roleLabel = message.role === "user" ? "You" : "Assistant";
+ // Only render markdown for assistant messages
+ const renderedContent = useMemo(() => {
+ if (message.role === "assistant") {
+ return marked.parse(message.content) as string;
+ }
+ return null;
+ }, [message.content, message.role]);
+
return (
{roleLabel}
- {message.content}
- {isStreaming &&
}
+ {message.role === "assistant" ? (
+ <>
+
+ {isStreaming &&
}
+ >
+ ) : (
+ message.content
+ )}
{message.citations && message.citations.length > 0 && (
diff --git a/apps/client/src/widgets/type_widgets/llm_chat/LlmChat.css b/apps/client/src/widgets/type_widgets/llm_chat/LlmChat.css
index 13c84658cc..79a6914c5d 100644
--- a/apps/client/src/widgets/type_widgets/llm_chat/LlmChat.css
+++ b/apps/client/src/widgets/type_widgets/llm_chat/LlmChat.css
@@ -48,11 +48,15 @@
}
.llm-chat-message-content {
- white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.5;
}
+/* Preserve whitespace only for user messages (plain text) */
+.llm-chat-message-user .llm-chat-message-content {
+ white-space: pre-wrap;
+}
+
.llm-chat-cursor {
display: inline-block;
width: 8px;
@@ -243,3 +247,121 @@
opacity: 0.5;
cursor: not-allowed;
}
+
+/* Markdown styles */
+.llm-chat-markdown {
+ line-height: 1.6;
+}
+
+.llm-chat-markdown p {
+ margin: 0 0 0.75em 0;
+}
+
+.llm-chat-markdown p:last-child {
+ margin-bottom: 0;
+}
+
+.llm-chat-markdown h1,
+.llm-chat-markdown h2,
+.llm-chat-markdown h3,
+.llm-chat-markdown h4,
+.llm-chat-markdown h5,
+.llm-chat-markdown h6 {
+ margin: 1em 0 0.5em 0;
+ font-weight: 600;
+ line-height: 1.3;
+}
+
+.llm-chat-markdown h1:first-child,
+.llm-chat-markdown h2:first-child,
+.llm-chat-markdown h3:first-child {
+ margin-top: 0;
+}
+
+.llm-chat-markdown h1 { font-size: 1.4em; }
+.llm-chat-markdown h2 { font-size: 1.25em; }
+.llm-chat-markdown h3 { font-size: 1.1em; }
+
+.llm-chat-markdown ul,
+.llm-chat-markdown ol {
+ margin: 0.5em 0;
+ padding-left: 1.5em;
+}
+
+.llm-chat-markdown li {
+ margin: 0.25em 0;
+}
+
+.llm-chat-markdown code {
+ background: var(--accented-background-color);
+ padding: 0.15em 0.4em;
+ border-radius: 4px;
+ font-family: var(--monospace-font-family, monospace);
+ font-size: 0.9em;
+}
+
+.llm-chat-markdown pre {
+ background: var(--accented-background-color);
+ padding: 0.75em 1em;
+ border-radius: 6px;
+ overflow-x: auto;
+ margin: 0.75em 0;
+}
+
+.llm-chat-markdown pre code {
+ background: none;
+ padding: 0;
+ font-size: 0.85em;
+}
+
+.llm-chat-markdown blockquote {
+ margin: 0.75em 0;
+ padding: 0.5em 1em;
+ border-left: 3px solid var(--main-border-color);
+ background: var(--accented-background-color);
+}
+
+.llm-chat-markdown blockquote p {
+ margin: 0;
+}
+
+.llm-chat-markdown a {
+ color: var(--link-color, #007bff);
+ text-decoration: none;
+}
+
+.llm-chat-markdown a:hover {
+ text-decoration: underline;
+}
+
+.llm-chat-markdown hr {
+ border: none;
+ border-top: 1px solid var(--main-border-color);
+ margin: 1em 0;
+}
+
+.llm-chat-markdown table {
+ border-collapse: collapse;
+ width: 100%;
+ margin: 0.75em 0;
+}
+
+.llm-chat-markdown th,
+.llm-chat-markdown td {
+ border: 1px solid var(--main-border-color);
+ padding: 0.5em 0.75em;
+ text-align: left;
+}
+
+.llm-chat-markdown th {
+ background: var(--accented-background-color);
+ font-weight: 600;
+}
+
+.llm-chat-markdown strong {
+ font-weight: 600;
+}
+
+.llm-chat-markdown em {
+ font-style: italic;
+}