From 9e429c4cd1876861fab347c60f9cb1b305719cfd Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Sat, 27 Sep 2025 13:23:28 +0100 Subject: [PATCH] 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. --- AIClasses/Gemini/Gemini.ts | 2 +- Components/ChatArea.svelte | 122 ++++++-- Components/ChatWindow.svelte | 4 +- Services/MarkdownService.ts | 112 ------- Services/ServiceRegistration.ts | 5 +- Services/StreamingMarkdownService.ts | 217 +++++++++++--- package-lock.json | 1 + package.json | 1 + styles.css | 426 ++++++++++++++++++++++++++- styles_old.css | 179 ----------- 10 files changed, 703 insertions(+), 366 deletions(-) delete mode 100644 Services/MarkdownService.ts delete mode 100644 styles_old.css diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index c749311..95ff7c7 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -42,7 +42,7 @@ export class Gemini implements IAIClass { "\n" + this.aiPrompt.responseFormat() + "\n" + - this.aiPrompt.getDirectories() + + //this.aiPrompt.getDirectories() + "\n" + prompt + "\n" + diff --git a/Components/ChatArea.svelte b/Components/ChatArea.svelte index 447d1ed..7286042 100644 --- a/Components/ChatArea.svelte +++ b/Components/ChatArea.svelte @@ -1,51 +1,119 @@
- {#each processedMessages as message (message.id)} + {#each messages as message (message.id)}
{#if message.isUser}

{message.content}

{:else} -
- {@html message.htmlContent} - {#if message.isStreaming} - ● ● ● - {/if} -
+
+ {#if message.isStreaming} + +
+ ● ● ● + {:else} + + {@html getStaticHTML(message)} + {/if} +
{/if}
@@ -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); diff --git a/Components/ChatWindow.svelte b/Components/ChatWindow.svelte index 74589b2..39c3d24 100644 --- a/Components/ChatWindow.svelte +++ b/Components/ChatWindow.svelte @@ -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); diff --git a/Services/MarkdownService.ts b/Services/MarkdownService.ts deleted file mode 100644 index c3fb2fc..0000000 --- a/Services/MarkdownService.ts +++ /dev/null @@ -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 | 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 `

${this.escapeHtml(content)}

`; - } - } - - 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; - } -} \ No newline at end of file diff --git a/Services/ServiceRegistration.ts b/Services/ServiceRegistration.ts index e88e7ec..b39be3e 100644 --- a/Services/ServiceRegistration.ts +++ b/Services/ServiceRegistration.ts @@ -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(Services.IPrompt, new AIPrompt()); RegisterSingleton(Services.IActioner, new Actioner()); - RegisterTransient(Services.MarkdownService, () => new MarkdownService()); RegisterTransient(Services.StreamingMarkdownService, () => new StreamingMarkdownService()); - RegisterAiProvider(plugin); } diff --git a/Services/StreamingMarkdownService.ts b/Services/StreamingMarkdownService.ts index 58c1d1d..39a467d 100644 --- a/Services/StreamingMarkdownService.ts +++ b/Services/StreamingMarkdownService.ts @@ -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 | null = null; + private streamingStates = new Map(); 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
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, '&') - .replace(//g, '>') - .replace(/\n/g, '
') - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/\*(.+?)\*/g, '$1'); + 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(); + + 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(/(?/g, '>'); + + 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(''); + inCodeBlock = false; + } else { + html.push('
');
+                    inCodeBlock = true;
+                }
+                continue;
+            }
+            
+            if (inCodeBlock) {
+                html.push(line + '\n');
+                continue;
+            }
+            
+            // Basic list support
+            if (/^[*+-]\s/.test(line)) {
+                if (!inList) {
+                    html.push('
    '); + inList = true; + } + html.push(`
  • ${line.substring(2)}
  • `); + } else if (inList && line.trim() === '') { + html.push('
'); + inList = false; + } else { + if (inList) { + html.push(''); + inList = false; + } + + // Basic formatting + const formatted = line + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\*(.+?)\*/g, '$1') + .replace(/`(.+?)`/g, '$1'); + + if (line.trim()) { + html.push(`

${formatted}

`); + } + } + } + + if (inList) html.push(''); + if (inCodeBlock) html.push('
'); + + return html.join(''); } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 81a477f..e0438b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index e052b6e..da58fe0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/styles.css b/styles.css index a90ed02..69002cb 100644 --- a/styles.css +++ b/styles.css @@ -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; + } } \ No newline at end of file diff --git a/styles_old.css b/styles_old.css deleted file mode 100644 index 2d8018a..0000000 --- a/styles_old.css +++ /dev/null @@ -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; -} \ No newline at end of file