From 4c48932ece4cfbc18c44add93e941efb1faa339a Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:15:21 +0000 Subject: [PATCH] fix: support inline formatting in table cells (bold, italic, code, links) --- .../src/lib/components/ChatInterface.svelte | 129 ++++++++---------- src/frontend/src/lib/utils/markdown.ts | 51 ++++++- 2 files changed, 99 insertions(+), 81 deletions(-) diff --git a/src/frontend/src/lib/components/ChatInterface.svelte b/src/frontend/src/lib/components/ChatInterface.svelte index bf59201..b930a09 100644 --- a/src/frontend/src/lib/components/ChatInterface.svelte +++ b/src/frontend/src/lib/components/ChatInterface.svelte @@ -128,22 +128,50 @@
- - {#each segment.headers as header} - - {/each} - - - - {#each segment.rows as row} - {#each row as cell} - + {#each segment.headers as header} + {/each} - - {/each} - -
{header}
{cell} + {#each header as cellSeg} + {#if cellSeg.type === 'bold'} + {cellSeg.content} + {:else if cellSeg.type === 'italic'} + {cellSeg.content} + {:else if cellSeg.type === 'code'} + {cellSeg.content} + {:else if cellSeg.type === 'link'} + {cellSeg.content} + {:else} + {cellSeg.content} + {/if} + {/each} +
+ + + + {#each segment.rows as row} + + {#each row as cell} + + {#each cell as cellSeg} + {#if cellSeg.type === 'bold'} + {cellSeg.content} + {:else if cellSeg.type === 'italic'} + {cellSeg.content} + {:else if cellSeg.type === 'code'} + {cellSeg.content} + {:else if cellSeg.type === 'link'} + {cellSeg.content} + {:else} + {cellSeg.content} + {/if} + {/each} + + {/each} + + {/each} + +
{:else if segment.type === 'heading'}

{segment.content}

@@ -158,7 +186,7 @@ {message.timestamp.toLocaleTimeString()} - {/each} + {/each} {#if isSending}
@@ -345,60 +373,6 @@ border: 1px solid rgba(251, 191, 36, 0.3); } - .message.thinking .message-content { - background: rgba(255, 255, 255, 0.05); - border: 1px dashed rgba(255, 255, 255, 0.2); - } - - .message-time { - font-size: 0.7rem; - color: #666; - margin-top: 0.25rem; - padding: 0 0.5rem; - } - - .thinking-header { - display: flex; - align-items: center; - margin-bottom: 0.5rem; - } - - .thinking-toggle { - display: flex; - align-items: center; - gap: 0.5rem; - background: none; - border: none; - color: #888; - cursor: pointer; - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-size: 0.8rem; - transition: background 0.2s; - } - - .thinking-toggle:hover { - background: rgba(255, 255, 255, 0.1); - } - - .thinking-icon { - font-size: 0.6rem; - } - - .thinking-label { - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.5px; - } - - .thinking-content { - color: #999; - font-size: 0.9rem; - padding-top: 0.5rem; - border-top: 1px solid rgba(255, 255, 255, 0.1); - margin-top: 0.5rem; - } - .inline-code { background: rgba(0, 0, 0, 0.3); padding: 0.15rem 0.4rem; @@ -430,11 +404,6 @@ margin: 0.25rem 0; } - a { - color: #667eea; - text-decoration: none; - } - .content-heading { font-size: 1rem; font-weight: 600; @@ -481,10 +450,22 @@ background: rgba(255, 255, 255, 0.05); } + a { + color: #667eea; + text-decoration: none; + } + a:hover { text-decoration: underline; } + .message-time { + font-size: 0.7rem; + color: #666; + margin-top: 0.25rem; + padding: 0 0.5rem; + } + .typing { display: flex; gap: 4px; diff --git a/src/frontend/src/lib/utils/markdown.ts b/src/frontend/src/lib/utils/markdown.ts index 95d07af..193490d 100644 --- a/src/frontend/src/lib/utils/markdown.ts +++ b/src/frontend/src/lib/utils/markdown.ts @@ -3,12 +3,18 @@ * Supports: bold, italic, code blocks, inline code, links, lists, tables, headings, line breaks */ +interface InlineSegment { + type: 'text' | 'bold' | 'italic' | 'code' | 'link'; + content: string; + href?: string; +} + interface ParsedSegment { type: 'text' | 'bold' | 'italic' | 'code' | 'codeBlock' | 'link' | 'list' | 'table' | 'lineBreak' | 'heading'; content: string; items?: string[]; - headers?: string[]; - rows?: string[][]; + headers?: InlineSegment[][]; + rows?: InlineSegment[][]; } export function parseMarkdown(text: string): ParsedSegment[] { @@ -79,7 +85,7 @@ function parseTable(tableStr: string): ParsedSegment[] { if (lines.length < 2) return []; // Skip separator line (|---|---|) - const dataLines = lines.filter(line => !line.match(/^[\|\s-]+$/)); + const dataLines = lines.filter(line => !line.match(/^[\|\s\-:]+$/)); if (dataLines.length < 2) return []; const headers = parseTableRow(dataLines[0]); @@ -93,8 +99,39 @@ function parseTable(tableStr: string): ParsedSegment[] { }]; } -function parseTableRow(row: string): string[] { - return row.split('|').map(cell => cell.trim()).filter(cell => cell !== ''); +function parseTableRow(row: string): InlineSegment[][] { + return row.split('|') + .map(cell => cell.trim()) + .filter(cell => cell !== '') + .map(cell => parseInlineElements(cell)); +} + +function parseInlineElements(text: string): InlineSegment[] { + const segments: InlineSegment[] = []; + + const inlineRegex = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`|\[.*?\]\(.*?\))/g; + const parts = text.split(inlineRegex); + + for (const part of parts) { + if (!part) continue; + + if (part.startsWith('**') && part.endsWith('**')) { + segments.push({ type: 'bold', content: part.slice(2, -2) }); + } else if (part.startsWith('*') && part.endsWith('*')) { + segments.push({ type: 'italic', content: part.slice(1, -1) }); + } else if (part.startsWith('`') && part.endsWith('`')) { + segments.push({ type: 'code', content: part.slice(1, -1) }); + } else if (part.startsWith('[') && part.includes('](')) { + const linkMatch = part.match(/\[(.*?)\]\((.*?)\)/); + if (linkMatch) { + segments.push({ type: 'link', content: linkMatch[1], href: linkMatch[2] }); + } + } else if (part) { + segments.push({ type: 'text', content: part }); + } + } + + return segments; } function parseLines(text: string): ParsedSegment[] { @@ -149,7 +186,7 @@ function parseLines(text: string): ParsedSegment[] { } // Process inline formatting - const inlineSegments = parseInlineElements(line); + const inlineSegments = parseInlineElementsAsText(line); segments.push(...inlineSegments); // Add line break after non-empty lines (except last in a paragraph) @@ -161,7 +198,7 @@ function parseLines(text: string): ParsedSegment[] { return segments; } -function parseInlineElements(text: string): ParsedSegment[] { +function parseInlineElementsAsText(text: string): ParsedSegment[] { const segments: ParsedSegment[] = []; const inlineRegex = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`|\[.*?\]\(.*?\))/g;