Refactor chat components and improve markdown processing

- Enhanced ChatArea component to handle streaming messages more efficiently with incremental updates.
- Removed MarkdownService as it was no longer needed; integrated its functionality into StreamingMarkdownService.
- Updated ServiceRegistration to reflect the removal of MarkdownService and adjusted AI class type.
- Improved StreamingMarkdownService for better streaming performance and error handling.
- Added new styles for syntax highlighting and markdown rendering in styles.css.
- Removed old styles_old.css as they were deprecated.
- Updated package.json and package-lock.json to include lowlight for improved syntax highlighting.
This commit is contained in:
Andrew Beal 2025-09-27 13:23:28 +01:00
parent 7efdac24cf
commit 9e429c4cd1
10 changed files with 703 additions and 366 deletions

View file

@ -42,7 +42,7 @@ export class Gemini implements IAIClass {
"\n" +
this.aiPrompt.responseFormat() +
"\n" +
this.aiPrompt.getDirectories() +
//this.aiPrompt.getDirectories() +
"\n" +
prompt +
"\n" +

View file

@ -1,51 +1,119 @@
<script lang="ts">
import { Resolve } from "Services/DependencyService";
import { MarkdownService } from "Services/MarkdownService";
import { Services } from "Services/Services";
import type { StreamingMarkdownService } from "Services/StreamingMarkdownService";
import type { StreamingMarkdownService } from "Services/StreamingMarkdownService";
export let messages: Array<{id: string, content: string, isUser: boolean, isStreaming: boolean}> = [];
let chatContainer: HTMLDivElement;
let streamingMarkdownService: StreamingMarkdownService = Resolve(Services.StreamingMarkdownService);
let messageElements = new Map<string, HTMLElement>();
let lastProcessedContent = new Map<string, string>();
// Process each message content with markdown
$: processedMessages = messages.map((message) => {
if (message.isUser) {
return {
...message,
htmlContent: `<p>${message.content}</p>`
};
// Track streaming messages and update them incrementally
$: {
messages.forEach((message) => {
if (!message.isUser) {
const lastContent = lastProcessedContent.get(message.id) || '';
// Only update if content has changed
if (message.content !== lastContent) {
updateMessageContent(message);
lastProcessedContent.set(message.id, message.content);
}
}
});
}
function updateMessageContent(message: {id: string, content: string, isUser: boolean, isStreaming: boolean}) {
const element = messageElements.get(message.id);
if (!element) return;
if (message.isStreaming) {
// Use streaming update
streamingMarkdownService.streamChunk(message.id, message.content);
} else {
let htmlContent;
// Finalize the message
streamingMarkdownService.finalizeStream(message.id, message.content);
}
}
function initializeMessageElement(messageId: string, element: HTMLElement) {
messageElements.set(messageId, element);
streamingMarkdownService.initializeStream(messageId, element);
}
// Svelte action to handle element initialization
function streamingAction(element: HTMLElement, messageId: string) {
initializeMessageElement(messageId, element);
return {
destroy() {
// Cleanup when element is removed
messageElements.delete(messageId);
}
};
}
// Process static messages (user messages and initial load)
function getStaticHTML(message: {id: string, content: string, isUser: boolean, isStreaming: boolean}): string {
if (message.isUser) {
return `<p>${message.content}</p>`;
}
// For assistant messages that aren't streaming, use traditional parsing
if (!message.isStreaming) {
try {
htmlContent = streamingMarkdownService.formatText(message.content) || `<p>${message.content}</p>`;
return streamingMarkdownService.formatText(message.content) || `<p>${message.content}</p>`;
} catch (err) {
console.error('HTML processing failed:', err);
htmlContent = `<p>${message.content}</p>`;
return `<p>${message.content}</p>`;
}
return {
...message,
htmlContent
};
}
});
return ''; // Streaming messages will be handled by the streaming service
}
// Clean up when component is destroyed
function cleanup() {
messageElements.clear();
lastProcessedContent.clear();
}
// Make sure to clean up when messages are removed
$: {
const currentMessageIds = new Set(messages.map(m => m.id));
// Remove tracking for messages that no longer exist
for (const [id] of messageElements) {
if (!currentMessageIds.has(id)) {
messageElements.delete(id);
lastProcessedContent.delete(id);
}
}
}
</script>
<div class="chat-area" bind:this={chatContainer}>
{#each processedMessages as message (message.id)}
{#each messages as message (message.id)}
<div class="message-container" class:user={message.isUser} class:assistant={!message.isUser}>
<div class="message-bubble" class:user={message.isUser} class:assistant={!message.isUser}>
{#if message.isUser}
<p>{message.content}</p>
{:else}
<div class="markdown-content" class:streaming={message.isStreaming}>
{@html message.htmlContent}
{#if message.isStreaming}
<span class="streaming-indicator">● ● ●</span>
{/if}
</div>
<div class="markdown-content" class:streaming={message.isStreaming}>
{#if message.isStreaming}
<!-- Streaming message: use action for initialization -->
<div
use:streamingAction={message.id}
class="streaming-content"
></div>
<span class="streaming-indicator">● ● ●</span>
{:else}
<!-- Static message: use traditional rendering -->
{@html getStaticHTML(message)}
{/if}
</div>
{/if}
</div>
</div>
@ -105,6 +173,10 @@
pointer-events: none;
}
.streaming-content {
min-height: 1em; /* Ensure the element exists for binding */
}
.streaming-indicator {
display: inline-block;
color: var(--text-accent);

View file

@ -4,9 +4,9 @@
import { Resolve } from "Services/DependencyService";
import { Services } from "Services/Services";
import ChatArea from "./ChatArea.svelte";
import type { IAIClassStreaming } from "AIClasses/Gemini/Gemini";
import type { IAIClass } from "AIClasses/IAIClass";
let ai: IAIClassStreaming = Resolve(Services.IAIClass);
let ai: IAIClass = Resolve(Services.IAIClass);
let actioner: IActioner = Resolve(Services.IActioner);
let semaphore: Semaphore = new Semaphore(1, false);

View file

@ -1,112 +0,0 @@
import { unified, type Processor } from 'unified';
import remarkParse from 'remark-parse';
import remarkMath from 'remark-math';
import remarkGfm from 'remark-gfm';
import remarkToc from 'remark-toc';
import remarkEmoji from 'remark-emoji';
import remarkRehype from 'remark-rehype';
import rehypeKatex from 'rehype-katex';
import rehypeHighlight from 'rehype-highlight';
import rehypeStringify from 'rehype-stringify';
export class MarkdownService {
private static processor: Processor<any, any, any, any, any> | null = null;
constructor() {
if (!MarkdownService.processor) {
MarkdownService.processor = this.createProcessor();
}
}
private createProcessor() {
return unified()
.use(remarkParse)
.use(remarkMath)
.use(remarkGfm)
.use(remarkToc)
.use(remarkEmoji)
.use(remarkRehype, { allowDangerousHtml: false })
.use(rehypeKatex)
.use(rehypeHighlight)
.use(rehypeStringify);
}
/**
* Formats LLM response text into HTML-friendly format
* Handles output from Claude, ChatGPT, and Gemini
* @param content - The raw LLM response text
* @returns HTML string
*/
formatToHTML(content: string): string {
try {
const preprocessedContent = this.preprocessContent(content);
const result = MarkdownService.processor!.processSync(preprocessedContent);
return String(result);
} catch (error) {
console.error('Markdown processing failed:', error);
return `<p>${this.escapeHtml(content)}</p>`;
}
}
private preprocessContent(content: string): string {
return content
// 1. LaTeX Math Notation (normalize different formats) - No change needed
.replace(/\\\[([\s\S]*?)\\\]/g, (_, equation) => `$$${equation}$$`)
.replace(/\\\(([\s\S]*?)\\\)/g, (_, equation) => `$${equation}$`)
// 2. Handle Code Block Language Tags (allow hyphens)
.replace(/```([a-zA-Z0-9-]+)\s*\n/g, '```$1\n')
// 3. Fix Common Escape Issues (use with caution) - No change, but be aware of its behavior
.replace(/\\`/g, '`')
.replace(/\\\*/g, '*')
.replace(/\\_/g, '_')
// 4. Normalize Line Endings - No change needed
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
// 5. Handle Triple Quotes - No change needed
.replace(/"""\s*(\w+)?\s*\n([\s\S]*?)\n"""/g, (match, lang, code) => {
return lang ? `\`\`\`${lang}\n${code}\n\`\`\`` : `\`\`\`\n${code}\n\`\`\``;
})
// 6. Clean up Multiple Consecutive Newlines (collapse to a standard paragraph break)
.replace(/\n{3,}/g, '\n\n')
// 7. Handle Gemini's math-in-code-blocks tendency - No change needed
.replace(/```math\n([\s\S]*?)\n```/g, (_, equation) => {
if (equation.includes('\\') || equation.includes('{') || equation.includes('^') || equation.includes('_')) {
return `$$${equation}$$`;
}
return `\`\`\`\n${equation}\n\`\`\``;
})
// 8. Fix Bold/Italic Formatting Issues - No change needed
.replace(/\*\*\s+([^*]+)\s+\*\*/g, '**$1**')
.replace(/\*\s+([^*]+)\s+\*/g, '*$1*')
// 9. Fix Table Formatting Issues - No change needed
.replace(/^\|\s+/gm, '| ')
.replace(/\s+\|$/gm, ' |')
// 10. Handle Alternative List Markers - No change needed
.replace(/^•\s/gm, '- ')
.replace(/^→\s/gm, '- ')
// 11. Fix Link Formatting Issues - No change needed
.replace(/\[\s+([^\]]+)\s+\]/g, '[$1]')
// 12. Handle Footnote Variations (remove optional spaces)
.replace(/\[\^(\w+)\]\s*:/g, '[^$1]:')
// 13. Clean up Heading Formatting - No change needed
.replace(/^#+\s+/gm, (match) => match.replace(/\s+/g, ' '));
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}

View file

@ -10,8 +10,7 @@ import type { IActioner } from "Actioner/IActioner";
import type { IAIClass } from "AIClasses/IAIClass";
import { GeminiActionDefinitions } from "Actioner/Gemini/GeminiActionDefinitions";
import type { IActionDefinitions } from "Actioner/IActionDefinitions";
import { Gemini, type IAIClassStreaming } from "AIClasses/Gemini/Gemini";
import { MarkdownService } from "./MarkdownService";
import { Gemini } from "AIClasses/Gemini/Gemini";
import { StreamingMarkdownService } from "./StreamingMarkdownService";
export function RegisterDependencies(plugin: DmsAssistantPlugin) {
@ -22,10 +21,8 @@ export function RegisterDependencies(plugin: DmsAssistantPlugin) {
RegisterSingleton<IPrompt>(Services.IPrompt, new AIPrompt());
RegisterSingleton<IActioner>(Services.IActioner, new Actioner());
RegisterTransient<MarkdownService>(Services.MarkdownService, () => new MarkdownService());
RegisterTransient<StreamingMarkdownService>(Services.StreamingMarkdownService, () => new StreamingMarkdownService());
RegisterAiProvider(plugin);
}

View file

@ -2,16 +2,22 @@ import { unified, type Processor } from 'unified';
import remarkParse from 'remark-parse';
import remarkMath from 'remark-math';
import remarkGfm from 'remark-gfm';
import remarkToc from 'remark-toc';
import remarkEmoji from 'remark-emoji';
import remarkRehype from 'remark-rehype';
import rehypeKatex from 'rehype-katex';
import rehypeHighlight from 'rehype-highlight';
import rehypeStringify from 'rehype-stringify';
import remarkBreaks from 'remark-breaks';
interface StreamingState {
element: HTMLElement;
buffer: string;
lastProcessedLength: number;
isComplete: boolean;
}
export class StreamingMarkdownService {
private static processor: Processor<any, any, any, any, any> | null = null;
private streamingStates = new Map<string, StreamingState>();
constructor() {
if (!StreamingMarkdownService.processor) {
@ -21,59 +27,198 @@ export class StreamingMarkdownService {
private createProcessor() {
return unified()
.use(remarkParse)
.use(remarkParse)
.use(remarkGfm)
.use(remarkBreaks) // Note: This makes single newlines into <br> tags.
.use(remarkToc)
.use(remarkEmoji)
.use(remarkMath)
.use(remarkRehype, { allowDangerousHtml: false }) // Important for security
.use(remarkRehype, {
allowDangerousHtml: false
})
.use(rehypeKatex)
.use(rehypeHighlight)
.use(rehypeStringify);
.use(rehypeHighlight, {
detect: true,
plainText: ['txt', 'text'],
aliases: {
javascript: ['js', 'jsx'],
typescript: ['ts', 'tsx'],
python: ['py'],
markdown: ['md', 'mdx'],
shell: ['bash', 'sh', 'zsh']
}
})
.use(rehypeStringify, {
allowDangerousHtml: false,
allowDangerousCharacters: false,
closeSelfClosing: true
});
}
public formatText(text: string) {
public formatText(text: string): string {
try {
const preprocessed = this.preprocessContent(text);
const result = StreamingMarkdownService.processor!.processSync(preprocessed);
return String(result);
} catch (error) {
console.warn('Markdown processing failed, falling back to safe rendering:', error);
// Fallback to basic HTML escaping if parsing fails
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>');
console.warn('Markdown processing failed:', error);
return this.getFallbackHTML(text);
}
}
// Simplified streaming approach
public initializeStream(messageId: string, container: HTMLElement): void {
container.innerHTML = '';
this.streamingStates.set(messageId, {
element: container,
buffer: '',
lastProcessedLength: 0,
isComplete: false
});
}
public streamChunk(messageId: string, fullText: string): void {
const state = this.streamingStates.get(messageId);
if (!state || state.isComplete) return;
// Update buffer
state.buffer = fullText;
// Use debounced rendering for better performance
this.debouncedRender(messageId);
}
private renderTimeouts = new Map<string, NodeJS.Timeout>();
private debouncedRender(messageId: string, immediate: boolean = false): void {
const existingTimeout = this.renderTimeouts.get(messageId);
if (existingTimeout) {
clearTimeout(existingTimeout);
}
const render = () => {
const state = this.streamingStates.get(messageId);
if (!state) return;
try {
const html = this.formatText(state.buffer);
state.element.innerHTML = html;
state.lastProcessedLength = state.buffer.length;
} catch (error) {
console.warn('Streaming render failed:', error);
}
this.renderTimeouts.delete(messageId);
};
if (immediate) {
render();
} else {
const timeout = setTimeout(render, 50); // 50ms debounce
this.renderTimeouts.set(messageId, timeout);
}
}
public finalizeStream(messageId: string, fullText: string): void {
const state = this.streamingStates.get(messageId);
if (!state) return;
state.isComplete = true;
state.buffer = fullText;
// Final render without debounce
this.debouncedRender(messageId, true);
// Cleanup
this.streamingStates.delete(messageId);
const timeout = this.renderTimeouts.get(messageId);
if (timeout) {
clearTimeout(timeout);
this.renderTimeouts.delete(messageId);
}
}
private preprocessContent(content: string): string {
// Simplified and safer preprocessing
return content
// Normalize line endings
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
// Convert LaTeX delimiters
.replace(/\\\[([\s\S]*?)\\\]/g, (match, math) => {
// Ensure math blocks are on their own lines
return '\n$$\n' + math.trim() + '\n$$\n';
})
.replace(/\\\(([\s\S]*?)\\\)/g, '$$$1$$')
// Ensure headers have blank lines before them (but not at start)
.replace(/([^\n])\n(#{1,6}\s)/g, '$1\n\n$2')
// Collapse excessive newlines but preserve intentional spacing
.replace(/\n{4,}/g, '\n\n\n')
// Clean up list formatting - ensure consistent spacing
.replace(/^(\s*)([*+-]|\d+\.)\s+/gm, '$1$2 ')
// Ensure task list checkboxes are properly formatted
.replace(/^(\s*)([*+-])\s*\[([ x])\]/gm, '$1$2 [$3]');
}
// Fix LaTeX math delimiters
.replace(/\\\[([\s\S]*?)\\\]/g, '$$\n$1\n$$') // Block math
.replace(/\\\(([\s\S]*?)\\\)/g, '$$1$') // Inline math - CRITICAL FIX HERE
// Clean up excessive newlines to a standard paragraph break
.replace(/\n{3,}/g, '\n\n')
// Fix escaped characters that shouldn't be (use with caution)
.replace(/\\\*/g, '*')
.replace(/\\_/g, '_')
.replace(/\\`/g, '`')
// Ensure headers have a blank line above them for proper parsing
.replace(/(?<!\n)\n(#{1,6}\s)/g, '\n\n$1')
// Clean up extra spaces inside formatting (robust version)
.replace(/\*\*\s+([^*]+?)\s+\*\*/g, '**$1**')
.replace(/(?<!\*)\*\s+([^*]+?)\s+\*(?!\*)/g, '*$1*');
private getFallbackHTML(text: string): string {
// Improved fallback with basic markdown support
const escaped = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
const lines = escaped.split('\n');
const html: string[] = [];
let inList = false;
let inCodeBlock = false;
for (const line of lines) {
if (line.startsWith('```')) {
if (inCodeBlock) {
html.push('</code></pre>');
inCodeBlock = false;
} else {
html.push('<pre><code>');
inCodeBlock = true;
}
continue;
}
if (inCodeBlock) {
html.push(line + '\n');
continue;
}
// Basic list support
if (/^[*+-]\s/.test(line)) {
if (!inList) {
html.push('<ul>');
inList = true;
}
html.push(`<li>${line.substring(2)}</li>`);
} else if (inList && line.trim() === '') {
html.push('</ul>');
inList = false;
} else {
if (inList) {
html.push('</ul>');
inList = false;
}
// Basic formatting
const formatted = line
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`(.+?)`/g, '<code>$1</code>');
if (line.trim()) {
html.push(`<p>${formatted}</p>`);
}
}
}
if (inList) html.push('</ul>');
if (inCodeBlock) html.push('</code></pre>');
return html.join('');
}
}

1
package-lock.json generated
View file

@ -14,6 +14,7 @@
"core-js": "^3.45.1",
"express": "^5.1.0",
"highlight.js": "^11.11.1",
"lowlight": "^3.3.0",
"rehype-highlight": "^7.0.2",
"rehype-katex": "^7.0.1",
"rehype-parse": "^9.0.1",

View file

@ -33,6 +33,7 @@
"core-js": "^3.45.1",
"express": "^5.1.0",
"highlight.js": "^11.11.1",
"lowlight": "^3.3.0",
"rehype-highlight": "^7.0.2",
"rehype-katex": "^7.0.1",
"rehype-parse": "^9.0.1",

View file

@ -1,3 +1,42 @@
/* ============================== */
/* CSS Variables for Theming */
/* ============================== */
:root {
--code-background: #24292e;
--code-background-inline: rgba(27, 31, 35, 0.05);
--text-color: #24292e;
--text-muted: #586069;
--link-color: #0969da;
--blockquote-border: #d0d7de;
--table-border: #d0d7de;
--table-header-background: #f6f8fa;
--hr-color: #d0d7de;
--checkbox-bg: #fff;
--checkbox-border: #d1d5da;
--checkbox-checked-bg: #0969da;
--footnote-border: #d0d7de;
}
/* Dark mode variables */
@media (prefers-color-scheme: dark) {
:root {
--code-background: #161b22;
--code-background-inline: rgba(240, 246, 252, 0.15);
--text-color: #c9d1d9;
--text-muted: #8b949e;
--link-color: #58a6ff;
--blockquote-border: #3b434b;
--table-border: #3b434b;
--table-header-background: #161b22;
--hr-color: #3b434b;
--checkbox-bg: #161b22;
--checkbox-border: #3b434b;
--checkbox-checked-bg: #58a6ff;
--footnote-border: #3b434b;
}
}
/* ============================== */
/* Syntax Highlighting Styles */
/* GitHub Dark Theme for highlight.js */
@ -11,6 +50,26 @@
background: #24292e;
}
/* Language indicator */
.hljs.language-js::before,
.hljs.language-javascript::before,
.hljs.language-python::before,
.hljs.language-css::before,
.hljs.language-html::before,
.hljs.language-typescript::before,
.hljs.language-jsx::before,
.hljs.language-tsx::before {
content: attr(class);
position: absolute;
top: 0;
right: 0;
padding: 0.25em 0.5em;
font-size: 0.75em;
color: #8b949e;
background: rgba(0, 0, 0, 0.3);
border-radius: 0 0 0 0.25em;
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
@ -107,12 +166,144 @@
color: #e1e4e8;
}
/* Additional highlight.js classes for better coverage */
.hljs-selector-pseudo {
color: #79b8ff;
}
.hljs-selector-tag {
color: #85e89d;
}
.hljs-template-comment {
color: #6a737d;
}
.hljs-meta-keyword {
color: #f97583;
}
.hljs-meta-string {
color: #9ecbff;
}
/* Alternative dark theme colors if preferred */
.hljs.github-dark-dimmed {
color: #adbac7;
background: #22272e;
}
/* ============================== */
/* GFM Task Lists */
/* ============================== */
/* Task list container */
.contains-task-list,
ul.contains-task-list {
list-style: none;
padding-left: 0;
}
/* Task list items */
.task-list-item,
li.task-list-item {
position: relative;
list-style: none;
padding-left: 1.75em;
margin-left: 0;
}
/* Task list checkboxes */
.task-list-item > input[type="checkbox"],
.task-list-item input[type="checkbox"] {
position: absolute;
left: 0;
top: 0.25em;
margin: 0;
width: 1em;
height: 1em;
cursor: pointer;
background-color: var(--checkbox-bg);
border: 1px solid var(--checkbox-border);
border-radius: 3px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
/* Checked state */
.task-list-item > input[type="checkbox"]:checked,
.task-list-item input[type="checkbox"]:checked {
background-color: var(--checkbox-checked-bg);
border-color: var(--checkbox-checked-bg);
}
/* Checkmark for checked boxes */
.task-list-item > input[type="checkbox"]:checked::after,
.task-list-item input[type="checkbox"]:checked::after {
content: "✓";
display: block;
text-align: center;
color: white;
font-size: 0.75em;
line-height: 1em;
font-weight: bold;
}
/* Disabled state */
.task-list-item > input[type="checkbox"]:disabled,
.task-list-item input[type="checkbox"]:disabled {
cursor: not-allowed;
opacity: 0.6;
}
/* Nested task lists */
.task-list-item .contains-task-list {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
/* ============================== */
/* GFM Footnotes */
/* ============================== */
/* Footnote references */
sup[data-footnote-ref],
a[data-footnote-ref] {
font-weight: bold;
text-decoration: none;
color: var(--link-color);
}
sup[data-footnote-ref]::before {
content: "[";
}
sup[data-footnote-ref]::after {
content: "]";
}
/* Footnote section */
section[data-footnotes],
.footnotes {
margin-top: 2em;
padding-top: 1em;
border-top: 1px solid var(--footnote-border);
font-size: 0.9em;
}
/* Footnote list */
section[data-footnotes] ol,
.footnotes ol {
padding-left: 1.5em;
}
/* Footnote back references */
a[data-footnote-backref] {
text-decoration: none;
margin-left: 0.25em;
}
/* ============================== */
/* KaTeX Math Rendering Styles */
/* ============================== */
@ -764,41 +955,48 @@ body {
font-size: 1.5em;
font-weight: 600;
margin: 0.67em 0;
line-height: 1.2;
}
.message-bubble.assistant h2 {
font-size: 1.3em;
font-weight: 600;
margin: 0.75em 0;
line-height: 1.2;
}
.message-bubble.assistant h3 {
font-size: 1.1em;
font-weight: 600;
margin: 0.83em 0;
line-height: 1.3;
}
.message-bubble.assistant h4 {
font-size: 1em;
font-weight: 600;
margin: 1.12em 0;
line-height: 1.3;
}
.message-bubble.assistant h5 {
font-size: 0.9em;
font-weight: 600;
margin: 1.5em 0;
line-height: 1.3;
}
.message-bubble.assistant h6 {
font-size: 0.85em;
font-weight: 600;
margin: 1.67em 0;
line-height: 1.3;
}
/* Paragraphs */
.message-bubble.assistant p {
margin: 0.5em 0;
line-height: 1.6;
}
/* Lists */
@ -806,23 +1004,66 @@ body {
.message-bubble.assistant ol {
margin: 0.5em 0;
padding-left: 2em;
list-style-position: outside;
}
.message-bubble.assistant ol {
list-style-type: decimal;
}
.message-bubble.assistant ul {
list-style-type: disc;
}
.message-bubble.assistant li {
margin: 0.25em 0;
padding-left: 0.25em;
display: list-item;
}
/* Handle nested lists properly */
.message-bubble.assistant ol ol,
.message-bubble.assistant ul ul,
.message-bubble.assistant ol ul,
.message-bubble.assistant ul ol {
margin: 0.25em 0;
padding-left: 1.5em;
}
/* Ensure nested list numbering works correctly */
.message-bubble.assistant ol ol {
list-style-type: lower-alpha;
}
.message-bubble.assistant ol ol ol {
list-style-type: lower-roman;
}
/* Fix paragraph spacing inside list items */
.message-bubble.assistant li > p {
margin: 0;
}
.message-bubble.assistant li > p:first-child {
margin-top: 0;
}
.message-bubble.assistant li > p:last-child {
margin-bottom: 0;
}
/* Code blocks */
.message-bubble.assistant pre {
background-color: var(--code-background, #f6f8fa);
background-color: var(--code-background);
border-radius: 4px;
padding: 0.5em;
overflow-x: auto;
margin: 0.5em 0;
position: relative;
}
.message-bubble.assistant code {
background-color: var(--code-background, rgba(27, 31, 35, 0.05));
background-color: var(--code-background-inline);
border-radius: 3px;
padding: 0.2em 0.4em;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
@ -832,11 +1073,12 @@ body {
.message-bubble.assistant pre code {
background-color: transparent;
padding: 0;
border-radius: 0;
}
/* Blockquotes */
.message-bubble.assistant blockquote {
border-left: 4px solid var(--blockquote-border, #d0d7de);
border-left: 4px solid var(--blockquote-border);
padding-left: 1em;
margin: 0.5em 0;
color: var(--text-muted);
@ -847,23 +1089,37 @@ body {
border-collapse: collapse;
margin: 0.5em 0;
width: 100%;
overflow-x: auto;
display: block;
}
.message-bubble.assistant th,
.message-bubble.assistant td {
border: 1px solid var(--table-border, #d0d7de);
border: 1px solid var(--table-border);
padding: 0.5em;
text-align: left;
min-width: 100px;
}
.message-bubble.assistant th {
background-color: var(--table-header-background, #f6f8fa);
background-color: var(--table-header-background);
font-weight: 600;
}
/* Table alignment classes from GFM */
.message-bubble.assistant th[align="center"],
.message-bubble.assistant td[align="center"] {
text-align: center;
}
.message-bubble.assistant th[align="right"],
.message-bubble.assistant td[align="right"] {
text-align: right;
}
/* Links */
.message-bubble.assistant a {
color: var(--link-color, #0969da);
color: var(--link-color);
text-decoration: none;
}
@ -874,7 +1130,7 @@ body {
/* Horizontal rules */
.message-bubble.assistant hr {
border: none;
border-top: 1px solid var(--hr-color, #d0d7de);
border-top: 1px solid var(--hr-color);
margin: 1em 0;
}
@ -898,4 +1154,160 @@ body {
/* Strikethrough */
.message-bubble.assistant del {
text-decoration: line-through;
opacity: 0.7;
}
/* ============================== */
/* Additional HTML Elements */
/* ============================== */
/* Keyboard input */
.message-bubble.assistant kbd {
display: inline-block;
padding: 0.2em 0.4em;
font-size: 0.875em;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
line-height: 1;
color: var(--text-color);
vertical-align: middle;
background-color: var(--code-background-inline);
border: 1px solid var(--table-border);
border-radius: 3px;
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);
}
/* Mark/highlight */
.message-bubble.assistant mark {
background-color: #fff3cd;
color: #000;
padding: 0.2em;
border-radius: 2px;
}
/* Subscript and Superscript */
.message-bubble.assistant sub {
font-size: 0.75em;
vertical-align: sub;
}
.message-bubble.assistant sup {
font-size: 0.75em;
vertical-align: super;
}
/* Abbreviations */
.message-bubble.assistant abbr[title] {
text-decoration: underline dotted;
cursor: help;
}
/* Definition lists */
.message-bubble.assistant dl {
margin: 0.5em 0;
}
.message-bubble.assistant dt {
font-weight: 600;
margin-top: 0.5em;
}
.message-bubble.assistant dd {
margin-left: 2em;
margin-bottom: 0.5em;
}
/* Details/Summary (collapsible sections) */
.message-bubble.assistant details {
margin: 0.5em 0;
padding: 0.5em;
border: 1px solid var(--table-border);
border-radius: 4px;
}
.message-bubble.assistant summary {
cursor: pointer;
font-weight: 600;
padding: 0.25em;
user-select: none;
}
.message-bubble.assistant details[open] summary {
margin-bottom: 0.5em;
}
/* ============================== */
/* Emoji Support */
/* ============================== */
.message-bubble.assistant .emoji {
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-weight: normal;
line-height: 1;
}
/* ============================== */
/* Line Numbers for Code Blocks (optional) */
/* ============================== */
/* If using line numbers with rehype-highlight */
.hljs .code-line {
display: block;
padding-left: 1em;
margin-left: -1em;
}
.hljs .numbered-code-line::before {
content: attr(data-line-number);
display: inline-block;
width: 2em;
text-align: right;
margin-right: 1em;
color: #6a737d;
user-select: none;
}
/* ============================== */
/* Responsive adjustments */
/* ============================== */
@media (max-width: 768px) {
.message-bubble.assistant table {
font-size: 0.875em;
}
.message-bubble.assistant th,
.message-bubble.assistant td {
padding: 0.25em;
min-width: 50px;
}
.message-bubble.assistant pre {
padding: 0.25em;
}
}
/* ============================== */
/* Print styles */
/* ============================== */
@media print {
.message-bubble.assistant {
color: #000;
}
.message-bubble.assistant a {
color: #0969da;
text-decoration: underline;
}
.message-bubble.assistant pre,
.message-bubble.assistant code {
background-color: #f6f8fa;
color: #000;
}
.hljs {
background: #fff;
color: #000;
}
}

View file

@ -1,179 +0,0 @@
/* Enhanced code block styling with better spacing */
pre {
background-color: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
padding: 1rem;
overflow-x: auto;
margin: 1rem 0;
line-height: 1.4; /* Better line spacing */
font-size: 0.875rem; /* Slightly larger for readability */
}
pre code {
font-family: var(--font-monospace);
font-size: inherit;
background: none; /* Remove any inherited background */
padding: 0; /* Remove any inherited padding */
border: none;
display: block;
white-space: pre; /* Preserve all whitespace and indentation */
word-wrap: normal;
overflow-wrap: normal;
}
/* Fix indentation preservation */
.hljs {
display: block;
overflow-x: auto;
padding: 0;
background: transparent;
tab-size: 4; /* Set consistent tab size */
-moz-tab-size: 4;
}
/* Ensure proper spacing for inline elements */
.hljs * {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
/* Syntax highlighting colors (keep your existing colors) */
.hljs-keyword, [data-token="keyword"] {
color: #ff7b72;
}
.hljs-string, [data-token="string"] {
color: #a5d6ff;
}
.hljs-comment, [data-token="comment"] {
color: #8b949e;
font-style: italic;
}
.hljs-number, [data-token="number"] {
color: #79c0ff;
}
.hljs-function, [data-token="function"] {
color: #d2a8ff;
}
.hljs-variable, [data-token="variable"] {
color: #ffa657;
}
.hljs-built_in, [data-token="builtin"] {
color: #ff7b72;
}
.hljs-title, [data-token="title"] {
color: #7ee787;
}
/* Additional fixes for better formatting */
.hljs-params {
color: var(--text-normal);
}
.hljs-attr {
color: #79c0ff;
}
.hljs-punctuation {
color: var(--text-muted);
}
/* Method/function calls */
.hljs-title.function_ {
color: #d2a8ff;
}
/* Variable names and identifiers */
.hljs-variable, .hljs-name {
color: #79c0ff;
}
/* Built-in functions and methods */
.hljs-built_in, .hljs-title.function {
color: #ffa657;
}
/* Property access and attributes */
.hljs-property, .hljs-attr {
color: #79c0ff;
}
/* Operators */
.hljs-operator {
color: #ff7b72;
}
/* Punctuation and delimiters */
.hljs-punctuation {
color: var(--text-normal);
}
/* Language-specific: Python function definitions */
.hljs-title.class_ {
color: #7ee787;
}
/* Parameters in function definitions */
.hljs-params {
color: #ffa657;
}
/* Literals and constants */
.hljs-literal, .hljs-constant {
color: #79c0ff;
}
/* Class names */
.hljs-class .hljs-title {
color: #7ee787;
}
/* Module/import names */
.hljs-module {
color: #ffa657;
}
/* Meta information (like decorators) */
.hljs-meta {
color: #8b949e;
}
/* Type annotations */
.hljs-type {
color: #7ee787;
}
.hljs-section { color: #7ee787; }
.hljs-tag { color: #ff7b72; }
.hljs-link { color: #a5d6ff; }
.hljs-subst { color: var(--text-normal); }
.hljs-formula { color: #a5d6ff; }
.hljs-addition { color: #7ee787; background-color: rgba(46, 160, 67, 0.15); }
.hljs-deletion { color: #ff7b72; background-color: rgba(248, 81, 73, 0.15); }
.hljs-selector-id, .hljs-selector-class { color: #7ee787; }
.hljs-doctag, .hljs-strong { font-weight: bold; }
.hljs-emphasis { font-style: italic; }
/* Ensure bold text renders properly */
strong, b {
font-weight: bold;
color: var(--text-normal);
}
em, i {
font-style: italic;
}
/* For markdown-rendered content specifically */
.markdown-rendered strong,
.markdown-rendered b {
font-weight: 600 !important;
}