Hemang Thakur commited on
Commit
2ef7092
·
1 Parent(s): 44ebcd1

ready to push

Browse files
frontend/src/Components/AiComponents/ChatComponents/Streaming.js CHANGED
@@ -1,8 +1,32 @@
1
  import React, { useEffect, useRef } from 'react';
2
- import CustomMarkdown from '../Markdown/CustomMarkdown';
 
 
 
 
3
  import './Streaming.css';
4
  import './SourceRef.css';
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  // Streaming component for rendering markdown content
7
  const Streaming = ({ content, isStreaming, onContentRef, showSourcePopup, hideSourcePopup }) => {
8
  const contentRef = useRef(null);
@@ -11,16 +35,168 @@ const Streaming = ({ content, isStreaming, onContentRef, showSourcePopup, hideSo
11
  if (contentRef.current && onContentRef) {
12
  onContentRef(contentRef.current);
13
  }
14
- }, [content, onContentRef]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  return (
17
  <div className="streaming-content" ref={contentRef}>
18
- <CustomMarkdown
19
- content={content}
20
- isStreaming={isStreaming}
21
- showSourcePopup={showSourcePopup}
22
- hideSourcePopup={hideSourcePopup}
23
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  </div>
25
  );
26
  };
 
1
  import React, { useEffect, useRef } from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4
+ import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
5
+ import remarkGfm from 'remark-gfm';
6
+ import rehypeRaw from 'rehype-raw';
7
  import './Streaming.css';
8
  import './SourceRef.css';
9
 
10
+ // Helper function to normalize various citation formats (e.g., [1,2], [1, 2]) into the standard [1][2] format
11
+ const normalizeCitations = (text) => {
12
+ if (!text) return '';
13
+
14
+ const citationRegex = /\[(\d+(?:,\s*\d+)+)\]/g;
15
+
16
+ return text.replace(citationRegex, (match, capturedNumbers) => {
17
+ const numbers = capturedNumbers
18
+ .split(/,\s*/)
19
+ .map(numStr => numStr.trim())
20
+ .filter(Boolean);
21
+
22
+ if (numbers.length <= 1) {
23
+ return match;
24
+ }
25
+
26
+ return numbers.map(num => `[${num}]`).join('');
27
+ });
28
+ };
29
+
30
  // Streaming component for rendering markdown content
31
  const Streaming = ({ content, isStreaming, onContentRef, showSourcePopup, hideSourcePopup }) => {
32
  const contentRef = useRef(null);
 
35
  if (contentRef.current && onContentRef) {
36
  onContentRef(contentRef.current);
37
  }
38
+ }, [content, onContentRef]);
39
+
40
+ const displayContent = isStreaming ? `${content}▌` : (content || '');
41
+ const normalizedContent = normalizeCitations(displayContent);
42
+
43
+ // Custom renderer for text nodes to handle source references
44
+ const renderWithSourceRefs = (elementType) => {
45
+ const ElementComponent = elementType; // e.g., 'p', 'li'
46
+
47
+ // Helper to gather plain text
48
+ const getFullText = (something) => {
49
+ if (typeof something === 'string') return something;
50
+ if (Array.isArray(something)) return something.map(getFullText).join('');
51
+ if (React.isValidElement(something) && something.props?.children)
52
+ return getFullText(React.Children.toArray(something.props.children));
53
+ return '';
54
+ };
55
+
56
+ return (props) => {
57
+ // Plain‑text version of this block (paragraph / list‑item)
58
+ const fullText = getFullText(props.children);
59
+ // Same regex the backend used
60
+ const sentenceRegex = /[^.!?\n]+[.!?]+[\])'"`’”]*|[^.!?\n]+$/g;
61
+ const sentencesArr = fullText.match(sentenceRegex) || [fullText];
62
+
63
+ // Helper function to find the sentence that contains position `pos`
64
+ const sentenceByPos = (pos) => {
65
+ let run = 0;
66
+ for (const s of sentencesArr) {
67
+ const end = run + s.length;
68
+ if (pos >= run && pos < end) return s.trim();
69
+ run = end;
70
+ }
71
+ return fullText.trim();
72
+ };
73
+
74
+ // Cursor that advances through fullText so each subsequent
75
+ // indexOf search starts AFTER the previous match
76
+ let searchCursor = 0;
77
+
78
+ // Recursive renderer that preserves existing markup
79
+ const processNode = (node, keyPrefix = 'node') => {
80
+ if (typeof node === 'string') {
81
+ const citationRegex = /\[(\d+)\]/g;
82
+ let last = 0;
83
+ let parts = [];
84
+ let m;
85
+ while ((m = citationRegex.exec(node))) {
86
+ const sliceBefore = node.slice(last, m.index);
87
+ if (sliceBefore) parts.push(sliceBefore);
88
+
89
+ const localIdx = m.index;
90
+ const num = parseInt(m[1], 10);
91
+ const citStr = m[0];
92
+
93
+ // Find this specific occurrence in fullText, starting at searchCursor
94
+ const absIdx = fullText.indexOf(citStr, searchCursor);
95
+ if (absIdx !== -1) searchCursor = absIdx + citStr.length;
96
+
97
+ const sentenceForPopup = sentenceByPos(absIdx);
98
+
99
+ parts.push(
100
+ <sup
101
+ key={`${keyPrefix}-ref-${num}-${localIdx}`}
102
+ className="source-reference"
103
+ onMouseEnter={(e) =>
104
+ showSourcePopup &&
105
+ showSourcePopup(num - 1, e.target, sentenceForPopup)
106
+ }
107
+ onMouseLeave={hideSourcePopup}
108
+ >
109
+ {num}
110
+ </sup>
111
+ );
112
+ last = localIdx + citStr.length;
113
+ }
114
+ if (last < node.length) parts.push(node.slice(last));
115
+ return parts;
116
+ }
117
+
118
+ // For non‑string children, recurse (preserves <em>, <strong>, links, etc.)
119
+ if (React.isValidElement(node) && node.props?.children) {
120
+ const processed = React.Children.map(node.props.children, (child, i) =>
121
+ processNode(child, `${keyPrefix}-${i}`)
122
+ );
123
+ return React.cloneElement(node, { children: processed });
124
+ }
125
+
126
+ return node; // element without children or unknown type
127
+ };
128
+
129
+ const processedChildren = React.Children.map(props.children, (child, i) =>
130
+ processNode(child, `root-${i}`)
131
+ );
132
+
133
+ // Render original element (p, li, …) with processed children
134
+ return <ElementComponent {...props}>{processedChildren}</ElementComponent>;
135
+ };
136
+ };
137
 
138
  return (
139
  <div className="streaming-content" ref={contentRef}>
140
+ <ReactMarkdown
141
+ remarkPlugins={[remarkGfm]}
142
+ rehypePlugins={[rehypeRaw]}
143
+ components={{
144
+ p: renderWithSourceRefs('p'),
145
+ li: renderWithSourceRefs('li'),
146
+ // Add other block elements if citations might appear directly within them
147
+ // blockquote: renderWithSourceRefs('blockquote'),
148
+ // div: renderWithSourceRefs('div'), // Be cautious with generic divs
149
+ code({node, inline, className, children, ...props}) {
150
+ const match = /language-(\w+)/.exec(className || '');
151
+ return !inline ? (
152
+ <div className="code-block-container">
153
+ <div className="code-block-header">
154
+ <span>{match ? match[1] : 'code'}</span>
155
+ </div>
156
+ <SyntaxHighlighter
157
+ style={atomDark}
158
+ language={match ? match[1] : 'text'}
159
+ PreTag="div"
160
+ {...props}
161
+ >
162
+ {String(children).replace(/\n$/, '')}
163
+ </SyntaxHighlighter>
164
+ </div>
165
+ ) : (
166
+ <code className={className} {...props}>
167
+ {children}
168
+ </code>
169
+ );
170
+ },
171
+ table({node, ...props}) {
172
+ return (
173
+ <div className="table-container">
174
+ <table {...props} />
175
+ </div>
176
+ );
177
+ },
178
+ a({node, children, href, ...props}) {
179
+ return (
180
+ <a
181
+ href={href}
182
+ target="_blank"
183
+ rel="noopener noreferrer"
184
+ className="markdown-link"
185
+ {...props}
186
+ >
187
+ {children}
188
+ </a>
189
+ );
190
+ },
191
+ blockquote({node, ...props}) {
192
+ return (
193
+ <blockquote className="markdown-blockquote" {...props} />
194
+ );
195
+ }
196
+ }}
197
+ >
198
+ {normalizedContent}
199
+ </ReactMarkdown>
200
  </div>
201
  );
202
  };
frontend/src/Components/AiComponents/Markdown/CustomMarkdown.js DELETED
@@ -1,489 +0,0 @@
1
- import React, { useEffect, useState, useCallback, useMemo } from 'react';
2
- import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
3
- import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
4
- import '../ChatComponents/Streaming.css';
5
- import '../ChatComponents/SourceRef.css';
6
-
7
- // Complete custom markdown parser and renderer that respects Streaming.css
8
- const CustomMarkdown = ({ content, isStreaming, showSourcePopup, hideSourcePopup }) => {
9
- const [parsedContent, setParsedContent] = useState([]);
10
-
11
- // Display content with cursor if streaming
12
- const displayContent = isStreaming ? `${content}▌` : (content || '');
13
-
14
- // Normalize citations like [1,2] to [1][2]
15
- const normalizeCitations = useCallback((text) => {
16
- if (!text) return '';
17
- const citationRegex = /\[(\d+(?:,\s*\d+)+)\]/g;
18
- return text.replace(citationRegex, (match, capturedNumbers) => {
19
- const numbers = capturedNumbers
20
- .split(/,\s*/)
21
- .map(numStr => numStr.trim())
22
- .filter(Boolean);
23
- if (numbers.length <= 1) return match;
24
- return numbers.map(num => `[${num}]`).join('');
25
- });
26
- }, []);
27
-
28
- const normalizedContent = useMemo(() => normalizeCitations(displayContent), [displayContent, normalizeCitations]);
29
-
30
- // Citation component
31
- const Citation = ({ number, showSourcePopup, hideSourcePopup, text }) => {
32
- const getSentenceForCitation = () => {
33
- const sentenceRegex = /[^.!?\n]+[.!?]+[\])'"`'"]*|[^.!?\n]+$/g;
34
- const sentences = text.match(sentenceRegex) || [text];
35
-
36
- for (const sentence of sentences) {
37
- if (sentence.includes(`[${number}]`)) {
38
- return sentence.trim();
39
- }
40
- }
41
- return '';
42
- };
43
-
44
- return (
45
- <sup
46
- className="source-reference"
47
- onMouseEnter={(e) => {
48
- if (showSourcePopup) {
49
- const sentence = getSentenceForCitation();
50
- showSourcePopup(number - 1, e.target, sentence);
51
- }
52
- }}
53
- onMouseLeave={hideSourcePopup}
54
- >
55
- {number}
56
- </sup>
57
- );
58
- };
59
-
60
- // Parse inline markdown elements
61
- const parseInline = useCallback((text) => {
62
- if (!text) return null;
63
-
64
- // Define regex patterns clearly
65
- const TWO_SPACES = ' '; // Exactly 2 spaces (not 1, not 3, but 2!)
66
- const lineBreakRegex = new RegExp(TWO_SPACES + '\\n', 'g');
67
-
68
- const elements = [];
69
- const patterns = [
70
- // Bold + Italic
71
- { regex: /\*\*\*(.+?)\*\*\*/g, handler: (m) => <strong key={m.index}><em>{parseInline(m[1])}</em></strong> },
72
- { regex: /___(.+?)___/g, handler: (m) => <strong key={m.index}><em>{parseInline(m[1])}</em></strong> },
73
- // Bold
74
- { regex: /\*\*(.+?)\*\*/g, handler: (m) => <strong key={m.index}>{parseInline(m[1])}</strong> },
75
- { regex: /__(.+?)__/g, handler: (m) => <strong key={m.index}>{parseInline(m[1])}</strong> },
76
- // Italic
77
- { regex: /\*([^*]+)\*/g, handler: (m) => <em key={m.index}>{parseInline(m[1])}</em> },
78
- { regex: /_([^_]+)_/g, handler: (m) => <em key={m.index}>{parseInline(m[1])}</em> },
79
- // Strikethrough
80
- { regex: /~~(.+?)~~/g, handler: (m) => <del key={m.index}>{parseInline(m[1])}</del> },
81
- // Inline code (preserve all spaces)
82
- { regex: /`([^`]+)`/g, handler: (m) => {
83
- // Preserve all whitespace in inline code
84
- const codeContent = m[1].replace(/ /g, '\u00A0'); // Replace spaces with non-breaking spaces
85
- return <code key={m.index}>{codeContent}</code>;
86
- }},
87
- // Images
88
- { regex: /!\[([^\]]*)\]\(([^)]+)\)/g, handler: (m) => <img key={m.index} src={m[2]} alt={m[1]} style={{ maxWidth: '100%' }} /> },
89
- // Links
90
- { regex: /\[([^\]]+)\]\(([^)]+)\)/g, handler: (m) => (
91
- <a key={m.index} href={m[2]} target="_blank" rel="noopener noreferrer" className="markdown-link">
92
- {parseInline(m[1])}
93
- </a>
94
- )},
95
- // Citations
96
- { regex: /\[(\d+)\]/g, handler: (m) => (
97
- <Citation
98
- key={m.index}
99
- number={parseInt(m[1], 10)}
100
- showSourcePopup={showSourcePopup}
101
- hideSourcePopup={hideSourcePopup}
102
- text={text}
103
- />
104
- )},
105
- // Line breaks
106
- {
107
- regex: lineBreakRegex,
108
- handler: (m) => <br key={m.index} />
109
- },
110
- ];
111
-
112
- // Apply patterns in order
113
- let processedText = text;
114
- const replacements = [];
115
-
116
- for (const pattern of patterns) {
117
- let match;
118
- pattern.regex.lastIndex = 0;
119
- while ((match = pattern.regex.exec(text))) {
120
- replacements.push({
121
- start: match.index,
122
- end: match.index + match[0].length,
123
- element: pattern.handler(match),
124
- priority: patterns.indexOf(pattern)
125
- });
126
- }
127
- }
128
-
129
- // Sort replacements by position and priority
130
- replacements.sort((a, b) => {
131
- if (a.start !== b.start) return a.start - b.start;
132
- return a.priority - b.priority;
133
- });
134
-
135
- // Build result without overlapping replacements
136
- let lastEnd = 0;
137
- const used = new Set();
138
-
139
- for (const replacement of replacements) {
140
- // Skip if this overlaps with an already used replacement
141
- let overlaps = false;
142
- for (const usedRange of used) {
143
- if (!(replacement.end <= usedRange.start || replacement.start >= usedRange.end)) {
144
- overlaps = true;
145
- break;
146
- }
147
- }
148
-
149
- if (!overlaps) {
150
- if (replacement.start > lastEnd) {
151
- // Preserve spaces in text segments
152
- const textSegment = processedText.substring(lastEnd, replacement.start);
153
- elements.push(textSegment);
154
- }
155
- elements.push(replacement.element);
156
- lastEnd = replacement.end;
157
- used.add({ start: replacement.start, end: replacement.end });
158
- }
159
- }
160
-
161
- if (lastEnd < processedText.length) {
162
- // Preserve spaces in remaining text
163
- elements.push(processedText.substring(lastEnd));
164
- }
165
-
166
- return elements.length > 0 ? elements : text;
167
- }, [showSourcePopup, hideSourcePopup]);
168
-
169
- // Parse code blocks separately to handle them properly
170
- const extractCodeBlocks = useCallback((text) => {
171
- const codeBlocks = [];
172
- const placeholder = '___CODE_BLOCK_';
173
- let counter = 0;
174
-
175
- // Replace code blocks with placeholders, preserve exact formatting
176
- const textWithoutCode = text.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
177
- const id = `${placeholder}${counter}___`;
178
- // Preserve exact code content without any trimming
179
- codeBlocks.push({ id, lang: lang || 'text', code: code.replace(/\n$/, '') }); // Only remove final newline
180
- counter++;
181
- return `\n${id}\n`;
182
- });
183
-
184
- return { textWithoutCode, codeBlocks };
185
- }, []);
186
-
187
- // Parse block-level elements
188
- const parseBlocks = useCallback((text, codeBlocks = []) => {
189
- if (!text) return [];
190
-
191
- const blocks = [];
192
- const lines = text.split('\n');
193
- let i = 0;
194
-
195
- while (i < lines.length) {
196
- const line = lines[i];
197
- const trimmedLine = line.trim();
198
-
199
- // Skip empty lines
200
- if (!trimmedLine) {
201
- i++;
202
- continue;
203
- }
204
-
205
- // Check for code block placeholder
206
- const codeBlockMatch = line.match(/___CODE_BLOCK_(\d+)___/);
207
- if (codeBlockMatch) {
208
- const codeBlock = codeBlocks.find(cb => cb.id === line.trim());
209
- if (codeBlock) {
210
- blocks.push({
211
- type: 'code',
212
- lang: codeBlock.lang,
213
- content: codeBlock.code
214
- });
215
- i++;
216
- continue;
217
- }
218
- }
219
-
220
- // Horizontal rule
221
- if (/^[-*_]{3,}$/.test(trimmedLine)) {
222
- blocks.push({ type: 'hr' });
223
- i++;
224
- continue;
225
- }
226
-
227
- // Headers
228
- const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
229
- if (headerMatch) {
230
- blocks.push({
231
- type: 'header',
232
- level: headerMatch[1].length,
233
- content: headerMatch[2]
234
- });
235
- i++;
236
- continue;
237
- }
238
-
239
- // Blockquotes
240
- if (line.startsWith('>')) {
241
- const quoteLines = [line.substring(1).trim()];
242
- i++;
243
- while (i < lines.length && (lines[i].startsWith('>') || lines[i].trim() === '')) {
244
- if (lines[i].startsWith('>')) {
245
- quoteLines.push(lines[i].substring(1).trim());
246
- } else if (lines[i].trim() === '' && i + 1 < lines.length && lines[i + 1].startsWith('>')) {
247
- quoteLines.push('');
248
- } else {
249
- break;
250
- }
251
- i++;
252
- }
253
- blocks.push({
254
- type: 'blockquote',
255
- content: quoteLines.join('\n')
256
- });
257
- continue;
258
- }
259
-
260
- // Lists (unordered and ordered)
261
- const unorderedMatch = line.match(/^([-*+])\s+(.+)$/);
262
- const orderedMatch = line.match(/^(\d+)\.\s+(.+)$/);
263
-
264
- if (unorderedMatch || orderedMatch) {
265
- const isOrdered = !!orderedMatch;
266
- const items = [];
267
- const listIndent = line.search(/\S/);
268
-
269
- while (i < lines.length) {
270
- const currentLine = lines[i];
271
- const currentIndent = currentLine.search(/\S/);
272
-
273
- if (currentIndent === -1) {
274
- // Empty line, check if list continues
275
- if (i + 1 < lines.length) {
276
- const nextIndent = lines[i + 1].search(/\S/);
277
- if (nextIndent >= listIndent && (lines[i + 1].match(/^[\s]*[-*+]\s+/) || lines[i + 1].match(/^[\s]*\d+\.\s+/))) {
278
- i++;
279
- continue;
280
- }
281
- }
282
- break;
283
- }
284
-
285
- const itemMatch = isOrdered
286
- ? currentLine.match(/^(\s*)\d+\.\s+(.+)$/)
287
- : currentLine.match(/^(\s*)[-*+]\s+(.+)$/);
288
-
289
- if (itemMatch && currentIndent === listIndent) {
290
- items.push({
291
- content: itemMatch[2],
292
- indent: 0
293
- });
294
- i++;
295
- } else if (currentIndent > listIndent) {
296
- // Continuation of previous item or nested list
297
- if (items.length > 0) {
298
- items[items.length - 1].content += '\n' + currentLine;
299
- }
300
- i++;
301
- } else {
302
- break;
303
- }
304
- }
305
-
306
- blocks.push({
307
- type: isOrdered ? 'ol' : 'ul',
308
- items: items.map(item => ({
309
- ...item,
310
- content: item.content.trim()
311
- }))
312
- });
313
- continue;
314
- }
315
-
316
- // Tables
317
- if (i + 1 < lines.length && lines[i + 1].trim().match(/^[-:|]+$/)) {
318
- const headerCells = line.split('|').map(cell => cell.trim()).filter(Boolean);
319
- const alignmentLine = lines[i + 1];
320
- const alignments = alignmentLine.split('|').map(cell => {
321
- const trimmed = cell.trim();
322
- if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';
323
- if (trimmed.endsWith(':')) return 'right';
324
- return 'left';
325
- }).filter((_, index) => index < headerCells.length);
326
-
327
- const rows = [];
328
- i += 2;
329
-
330
- while (i < lines.length && lines[i].includes('|')) {
331
- const cells = lines[i].split('|').map(cell => cell.trim()).filter(Boolean);
332
- if (cells.length > 0) {
333
- rows.push(cells);
334
- }
335
- i++;
336
- }
337
-
338
- blocks.push({
339
- type: 'table',
340
- headers: headerCells,
341
- alignments,
342
- rows
343
- });
344
- continue;
345
- }
346
-
347
- // Paragraph
348
- const paragraphLines = [line];
349
- i++;
350
- while (i < lines.length && lines[i].trim() !== '' &&
351
- !lines[i].match(/^#{1,6}\s/) &&
352
- !lines[i].match(/^[-*+]\s/) &&
353
- !lines[i].match(/^\d+\.\s/) &&
354
- !lines[i].startsWith('>') &&
355
- !lines[i].match(/^[-*_]{3,}$/) &&
356
- !lines[i].match(/___CODE_BLOCK_\d+___/)) {
357
- paragraphLines.push(lines[i]);
358
- i++;
359
- }
360
-
361
- blocks.push({
362
- type: 'paragraph',
363
- content: paragraphLines.join('\n')
364
- });
365
- }
366
-
367
- return blocks;
368
- }, []);
369
-
370
- // Render a single block
371
- const renderBlock = useCallback((block, index) => {
372
- switch (block.type) {
373
- case 'header':
374
- const HeaderTag = `h${block.level}`;
375
- return <HeaderTag key={index}>{parseInline(block.content)}</HeaderTag>;
376
-
377
- case 'paragraph':
378
- return <p key={index}>{parseInline(block.content)}</p>;
379
-
380
- case 'blockquote':
381
- const { textWithoutCode, codeBlocks } = extractCodeBlocks(block.content);
382
- const quotedBlocks = parseBlocks(textWithoutCode, codeBlocks);
383
- return (
384
- <blockquote key={index} className="markdown-blockquote">
385
- {quotedBlocks.map((b, i) => renderBlock(b, i))}
386
- </blockquote>
387
- );
388
-
389
- case 'code':
390
- return (
391
- <div key={index} className="code-block-container">
392
- <div className="code-block-header">
393
- <span>{block.lang}</span>
394
- </div>
395
- <SyntaxHighlighter
396
- style={atomDark}
397
- language={block.lang}
398
- PreTag="div"
399
- customStyle={{ margin: 0 }}
400
- >
401
- {block.content}
402
- </SyntaxHighlighter>
403
- </div>
404
- );
405
-
406
- case 'ul':
407
- case 'ol':
408
- const ListTag = block.type === 'ol' ? 'ol' : 'ul';
409
- return (
410
- <ListTag key={index}>
411
- {block.items.map((item, i) => {
412
- // Handle nested content properly
413
- if (item.content.includes('\n')) {
414
- // For multi-line items, parse as nested markdown
415
- const { textWithoutCode, codeBlocks } = extractCodeBlocks(item.content);
416
- const nestedBlocks = parseBlocks(textWithoutCode, codeBlocks);
417
- return (
418
- <li key={i}>
419
- {nestedBlocks.map((b, j) => renderBlock(b, `${i}-${j}`))}
420
- </li>
421
- );
422
- }
423
- // For single-line items, just parse inline
424
- return <li key={i}>{parseInline(item.content)}</li>;
425
- })}
426
- </ListTag>
427
- );
428
-
429
- case 'table':
430
- return (
431
- <div key={index} className="table-container">
432
- <table>
433
- <thead>
434
- <tr>
435
- {block.headers.map((header, i) => (
436
- <th key={i} style={{ textAlign: block.alignments[i] || 'left' }}>
437
- {parseInline(header)}
438
- </th>
439
- ))}
440
- </tr>
441
- </thead>
442
- <tbody>
443
- {block.rows.map((row, rowIndex) => (
444
- <tr key={rowIndex}>
445
- {row.map((cell, cellIndex) => (
446
- <td key={cellIndex} style={{ textAlign: block.alignments[cellIndex] || 'left' }}>
447
- {parseInline(cell)}
448
- </td>
449
- ))}
450
- </tr>
451
- ))}
452
- </tbody>
453
- </table>
454
- </div>
455
- );
456
-
457
- case 'hr':
458
- return <hr key={index} />;
459
-
460
- default:
461
- return null;
462
- }
463
- }, [parseInline, extractCodeBlocks, parseBlocks]);
464
-
465
- // Main parse function
466
- const parseMarkdown = useCallback((text) => {
467
- if (!text) return [];
468
-
469
- // Extract code blocks first
470
- const { textWithoutCode, codeBlocks } = extractCodeBlocks(text);
471
-
472
- // Parse blocks
473
- const blocks = parseBlocks(textWithoutCode, codeBlocks);
474
-
475
- // Render blocks
476
- return blocks.map((block, index) => renderBlock(block, index));
477
- }, [extractCodeBlocks, parseBlocks, renderBlock]);
478
-
479
- // Parse markdown content whenever it changes
480
- useEffect(() => {
481
- const parsed = parseMarkdown(normalizedContent);
482
- setParsedContent(parsed);
483
- }, [normalizedContent, parseMarkdown]);
484
-
485
- // Return just the parsed content
486
- return <>{parsedContent}</>;
487
- };
488
-
489
- export default CustomMarkdown;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/Components/AiComponents/Markdown/TestMarkdown.js DELETED
@@ -1,120 +0,0 @@
1
- import CustomMarkdown from './CustomMarkdown';
2
-
3
- const SpacePreservationTest = () => {
4
- const testContent = `# Space Preservation Test
5
-
6
- ## Inline Code Spacing
7
-
8
- Here are examples with multiple spaces:
9
-
10
- - One space: \`a b\`
11
- - Two spaces: \`a b\`
12
- - Three spaces: \`a b\`
13
- - Four spaces: \`a b\`
14
- - Tab character: \`a b\`
15
- - Mixed: \`function ( x, y )\`
16
-
17
- ## Code Block Indentation
18
-
19
- \`\`\`python
20
- def example():
21
- # 4 spaces indentation
22
- if True:
23
- # 8 spaces indentation
24
- print("Hello")
25
-
26
- # Empty line above preserved
27
- for i in range(5):
28
- # Aligned comments
29
- print(i) # End of line comment
30
- \`\`\`
31
-
32
- ## ASCII Art Test
33
-
34
- \`\`\`
35
- _____
36
- / ___ \\
37
- | | | |
38
- | |___| |
39
- \\_____/
40
-
41
- Spacing matters!
42
- \`\`\`
43
-
44
- ## Table Alignment
45
-
46
- \`\`\`
47
- Name Age City
48
- ---- --- ----
49
- Alice 25 NYC
50
- Bob 30 LA
51
- Charlie 35 Chicago
52
- \`\`\`
53
-
54
- ## Inline Examples
55
-
56
- The function \`map( x => x * 2 )\` has spaces around the arrow.
57
-
58
- Configuration: \`{ indent: 4, tabs: false }\``;
59
-
60
- return (
61
- <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
62
- <h1>Space Preservation Test</h1>
63
-
64
- <div style={{
65
- marginBottom: '20px',
66
- padding: '15px',
67
- background: '#e3f2fd',
68
- borderRadius: '8px'
69
- }}>
70
- <p><strong>What to check:</strong></p>
71
- <ul>
72
- <li>Inline code should preserve exact spacing</li>
73
- <li>Code blocks should maintain indentation</li>
74
- <li>ASCII art should be properly aligned</li>
75
- <li>Empty lines in code blocks should be preserved</li>
76
- </ul>
77
- </div>
78
-
79
- <div style={{
80
- border: '2px solid #333',
81
- borderRadius: '8px',
82
- background: 'white',
83
- padding: '20px'
84
- }}>
85
- <CustomMarkdown
86
- content={testContent}
87
- isStreaming={false}
88
- showSourcePopup={() => {}}
89
- hideSourcePopup={() => {}}
90
- />
91
- </div>
92
-
93
- <div style={{
94
- marginTop: '20px',
95
- padding: '15px',
96
- background: '#f5f5f5',
97
- borderRadius: '8px',
98
- fontFamily: 'monospace',
99
- fontSize: '14px'
100
- }}>
101
- <p><strong>Debug: Raw content preview</strong></p>
102
- <pre style={{
103
- background: '#333',
104
- color: '#fff',
105
- padding: '10px',
106
- borderRadius: '4px',
107
- overflow: 'auto',
108
- whiteSpace: 'pre'
109
- }}>
110
- {`'a b' = one space
111
- 'a b' = two spaces
112
- 'a b' = three spaces
113
- 'a b' = four spaces`}
114
- </pre>
115
- </div>
116
- </div>
117
- );
118
- };
119
-
120
- export default SpacePreservationTest;