LiamKhoaLe commited on
Commit
0d58cb1
·
1 Parent(s): 2908396

Upd code and diagram viewer + pdf

Browse files
helpers/diagram.py CHANGED
@@ -27,7 +27,7 @@ async def generate_mermaid_diagram(
27
  nvidia_rotator,
28
  render_error: str = "",
29
  retry: int = 0,
30
- max_retries: int = 2
31
  ) -> str:
32
  from utils.api.router import generate_answer_with_model
33
 
@@ -40,24 +40,36 @@ async def generate_mermaid_diagram(
40
  overview.append(f"{section_id} {title}: {syn[:180]}...")
41
  context_overview = "\n".join(overview)
42
 
 
43
  sys_prompt = (
44
- "You are an expert technical illustrator. Create a single concise Mermaid diagram that best conveys the core structure\n"
45
  "(e.g., flowchart, sequence, class, state, or ER) based on the provided CONTEXT.\n"
46
  "Rules:\n"
47
  "- Return Mermaid code only (no backticks, no explanations).\n"
48
  "- Prefer flowchart or sequence if uncertain.\n"
49
  "- Keep node labels short but meaningful.\n"
50
- "- Ensure Mermaid syntax is valid.\n"
 
 
 
 
 
51
  )
52
 
53
- feedback = (f"\n\nRENDERING ERROR TO FIX: {render_error}\n" if render_error else "")
 
 
 
 
 
54
  user_prompt = (
55
  f"INSTRUCTIONS:\n{instructions}\n\nCONTEXT OVERVIEW:\n{context_overview}{feedback}"
56
  )
57
 
 
58
  selection = {"provider": "nvidia_large", "model": "openai/gpt-oss-120b"}
59
 
60
- logger.info(f"[DIAGRAM] Generating Mermaid (retry={retry})")
61
  diagram = await generate_answer_with_model(selection, sys_prompt, user_prompt, gemini_rotator, nvidia_rotator)
62
  diagram = (diagram or "").strip()
63
 
@@ -67,7 +79,7 @@ async def generate_mermaid_diagram(
67
  if raw.lower().startswith("mermaid"):
68
  diagram = "\n".join(raw.splitlines()[1:])
69
 
70
- # Naive validation: basic mermaid keywords
71
  if not any(kw in diagram for kw in ("graph", "sequenceDiagram", "classDiagram", "stateDiagram", "erDiagram")):
72
  logger.warning("[DIAGRAM] Mermaid validation failed: missing diagram keywords")
73
  if retry < max_retries:
@@ -79,3 +91,176 @@ async def generate_mermaid_diagram(
79
  return diagram
80
 
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  nvidia_rotator,
28
  render_error: str = "",
29
  retry: int = 0,
30
+ max_retries: int = 5
31
  ) -> str:
32
  from utils.api.router import generate_answer_with_model
33
 
 
40
  overview.append(f"{section_id} {title}: {syn[:180]}...")
41
  context_overview = "\n".join(overview)
42
 
43
+ # Enhanced system prompt with better error handling guidance
44
  sys_prompt = (
45
+ "You are an expert technical illustrator and Mermaid syntax specialist. Create a single concise Mermaid diagram that best conveys the core structure\n"
46
  "(e.g., flowchart, sequence, class, state, or ER) based on the provided CONTEXT.\n"
47
  "Rules:\n"
48
  "- Return Mermaid code only (no backticks, no explanations).\n"
49
  "- Prefer flowchart or sequence if uncertain.\n"
50
  "- Keep node labels short but meaningful.\n"
51
+ "- Ensure Mermaid syntax is valid and follows these guidelines:\n"
52
+ " * Use proper node IDs (alphanumeric, no spaces)\n"
53
+ " * Use proper arrow syntax (--> for flowcharts, ->> for sequence)\n"
54
+ " * Quote labels with special characters\n"
55
+ " * Use proper diagram type declarations\n"
56
+ "- If there was a previous error, fix the specific syntax issues mentioned.\n"
57
  )
58
 
59
+ # Enhanced error feedback
60
+ if render_error:
61
+ feedback = f"\n\nPREVIOUS RENDERING ERROR TO FIX:\n{render_error}\n\nPlease analyze this error and generate a corrected Mermaid diagram that addresses the specific syntax or logical issues mentioned above."
62
+ else:
63
+ feedback = ""
64
+
65
  user_prompt = (
66
  f"INSTRUCTIONS:\n{instructions}\n\nCONTEXT OVERVIEW:\n{context_overview}{feedback}"
67
  )
68
 
69
+ # Use NVIDIA_LARGE for better diagram generation
70
  selection = {"provider": "nvidia_large", "model": "openai/gpt-oss-120b"}
71
 
72
+ logger.info(f"[DIAGRAM] Generating Mermaid (retry={retry}/{max_retries})")
73
  diagram = await generate_answer_with_model(selection, sys_prompt, user_prompt, gemini_rotator, nvidia_rotator)
74
  diagram = (diagram or "").strip()
75
 
 
79
  if raw.lower().startswith("mermaid"):
80
  diagram = "\n".join(raw.splitlines()[1:])
81
 
82
+ # Enhanced validation: check for common Mermaid syntax issues
83
  if not any(kw in diagram for kw in ("graph", "sequenceDiagram", "classDiagram", "stateDiagram", "erDiagram")):
84
  logger.warning("[DIAGRAM] Mermaid validation failed: missing diagram keywords")
85
  if retry < max_retries:
 
91
  return diagram
92
 
93
 
94
+ async def _render_mermaid_with_retry(mermaid_text: str, max_retries: int = 3) -> bytes:
95
+ """
96
+ Render mermaid code to PNG with retry logic and AI-powered error correction.
97
+ """
98
+ last_error = ""
99
+
100
+ for attempt in range(max_retries):
101
+ try:
102
+ # Try to render the current mermaid code
103
+ img_bytes = _render_mermaid_png(mermaid_text)
104
+
105
+ if img_bytes and len(img_bytes) > 0:
106
+ logger.info(f"[DIAGRAM] Mermaid rendered successfully on attempt {attempt + 1}")
107
+ return img_bytes
108
+ else:
109
+ logger.warning(f"[DIAGRAM] Mermaid render returned empty on attempt {attempt + 1}")
110
+
111
+ except Exception as e:
112
+ last_error = str(e)
113
+ logger.warning(f"[DIAGRAM] Mermaid render attempt {attempt + 1} failed: {e}")
114
+
115
+ # If this isn't the last attempt, try to fix the mermaid code using AI
116
+ if attempt < max_retries - 1:
117
+ try:
118
+ logger.info(f"[DIAGRAM] Attempting to fix Mermaid syntax using AI (attempt {attempt + 1})")
119
+ fixed_mermaid = await _fix_mermaid_with_ai(mermaid_text, last_error)
120
+ if fixed_mermaid and fixed_mermaid != mermaid_text:
121
+ mermaid_text = fixed_mermaid
122
+ logger.info(f"[DIAGRAM] AI provided fixed Mermaid code for retry {attempt + 2}")
123
+ else:
124
+ logger.warning(f"[DIAGRAM] AI could not provide fixed Mermaid code")
125
+ break
126
+ except Exception as ai_error:
127
+ logger.warning(f"[DIAGRAM] AI Mermaid fix failed: {ai_error}")
128
+ break
129
+
130
+ logger.warning(f"[DIAGRAM] All Mermaid render attempts failed, last error: {last_error}")
131
+ return b""
132
+
133
+
134
+ async def _fix_mermaid_with_ai(mermaid_text: str, error_message: str) -> str:
135
+ """
136
+ Use AI to fix Mermaid syntax errors.
137
+ """
138
+ try:
139
+ from utils.api.router import generate_answer_with_model
140
+
141
+ sys_prompt = """You are a Mermaid syntax expert. Your task is to fix Mermaid diagram syntax errors.
142
+
143
+ Rules:
144
+ 1. Return ONLY the corrected Mermaid code (no backticks, no explanations)
145
+ 2. Ensure proper syntax for the diagram type
146
+ 3. Fix common issues like:
147
+ - Invalid node IDs (use alphanumeric, no spaces)
148
+ - Incorrect arrow syntax
149
+ - Missing quotes around labels with special characters
150
+ - Wrong diagram type declarations
151
+ 4. Maintain the original intent and structure of the diagram"""
152
+
153
+ user_prompt = f"""Fix this Mermaid diagram that has rendering errors:
154
+
155
+ ORIGINAL MERMAID CODE:
156
+ ```mermaid
157
+ {mermaid_text}
158
+ ```
159
+
160
+ ERROR MESSAGE:
161
+ {error_message}
162
+
163
+ Please provide the corrected Mermaid code that will render successfully."""
164
+
165
+ # Use NVIDIA_LARGE for better error correction
166
+ selection = {"provider": "nvidia_large", "model": "openai/gpt-oss-120b"}
167
+ response = await generate_answer_with_model(selection, sys_prompt, user_prompt, None, None)
168
+
169
+ if response:
170
+ # Clean up the response
171
+ fixed_code = response.strip()
172
+ if fixed_code.startswith("```"):
173
+ fixed_code = fixed_code.strip('`')
174
+ if fixed_code.lower().startswith("mermaid"):
175
+ fixed_code = "\n".join(fixed_code.splitlines()[1:])
176
+
177
+ return fixed_code.strip()
178
+
179
+ except Exception as e:
180
+ logger.warning(f"[DIAGRAM] AI Mermaid fix failed: {e}")
181
+
182
+ return ""
183
+
184
+
185
+ def _render_mermaid_png(mermaid_text: str) -> bytes:
186
+ """
187
+ Render mermaid code to PNG via Kroki service (no local mermaid-cli dependency).
188
+ Falls back to returning empty bytes on failure.
189
+ """
190
+ try:
191
+ import base64
192
+ import json
193
+ import urllib.request
194
+ import urllib.error
195
+
196
+ # Validate and clean mermaid content
197
+ if not mermaid_text or not mermaid_text.strip():
198
+ logger.warning("[DIAGRAM] Empty mermaid content")
199
+ return b""
200
+
201
+ # Clean the mermaid text - remove any potential issues
202
+ cleaned_text = mermaid_text.strip()
203
+
204
+ # Basic mermaid syntax validation
205
+ if not cleaned_text.startswith(('graph', 'flowchart', 'sequenceDiagram', 'classDiagram', 'stateDiagram', 'erDiagram', 'journey', 'gantt', 'pie', 'gitgraph')):
206
+ logger.warning(f"[DIAGRAM] Invalid mermaid diagram type: {cleaned_text[:50]}...")
207
+ return b""
208
+
209
+ # Kroki POST API for mermaid -> png
210
+ data = json.dumps({"diagram_source": cleaned_text}).encode("utf-8")
211
+ req = urllib.request.Request(
212
+ url="https://kroki.io/mermaid/png",
213
+ data=data,
214
+ headers={"Content-Type": "application/json"},
215
+ method="POST"
216
+ )
217
+
218
+ with urllib.request.urlopen(req, timeout=15) as resp:
219
+ if resp.status == 200:
220
+ return resp.read()
221
+ else:
222
+ logger.warning(f"[DIAGRAM] Kroki returned status {resp.status}")
223
+ return b""
224
+
225
+ except urllib.error.HTTPError as e:
226
+ if e.code == 400:
227
+ logger.warning(f"[DIAGRAM] Kroki mermaid syntax error (400): {e.reason}")
228
+ else:
229
+ logger.warning(f"[DIAGRAM] Kroki HTTP error {e.code}: {e.reason}")
230
+ except urllib.error.URLError as e:
231
+ logger.warning(f"[DIAGRAM] Kroki connection error: {e.reason}")
232
+ except Exception as e:
233
+ logger.warning(f"[DIAGRAM] Kroki mermaid render error: {e}")
234
+
235
+ return b""
236
+
237
+
238
+ async def fix_mermaid_syntax_for_ui(mermaid_text: str, error_message: str = "") -> str:
239
+ """
240
+ Fix Mermaid syntax for UI rendering using AI.
241
+ Returns the corrected Mermaid code that can be used in the browser.
242
+ """
243
+ try:
244
+ # If no error message provided, try to validate the mermaid syntax first
245
+ if not error_message:
246
+ # Basic validation - check for common issues
247
+ if not mermaid_text.strip():
248
+ error_message = "Empty Mermaid diagram"
249
+ elif not any(kw in mermaid_text for kw in ("graph", "sequenceDiagram", "classDiagram", "stateDiagram", "erDiagram")):
250
+ error_message = "Missing valid Mermaid diagram type declaration"
251
+
252
+ # Use AI to fix the mermaid code
253
+ fixed_code = await _fix_mermaid_with_ai(mermaid_text, error_message)
254
+
255
+ if fixed_code and fixed_code != mermaid_text:
256
+ logger.info(f"[DIAGRAM] AI provided fixed Mermaid code for UI")
257
+ return fixed_code
258
+ else:
259
+ logger.warning(f"[DIAGRAM] AI could not fix Mermaid code for UI")
260
+ return mermaid_text # Return original if AI couldn't fix it
261
+
262
+ except Exception as e:
263
+ logger.warning(f"[DIAGRAM] Mermaid UI fix failed: {e}")
264
+ return mermaid_text # Return original on error
265
+
266
+
routes/reports.py CHANGED
@@ -1296,3 +1296,32 @@ Return the renumbered headings in the format: "level: new_number: heading_text"
1296
  return report
1297
 
1298
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1296
  return report
1297
 
1298
 
1299
+ @app.post("/mermaid/fix")
1300
+ async def fix_mermaid_syntax(
1301
+ mermaid_code: str = Form(...),
1302
+ error_message: str = Form("")
1303
+ ):
1304
+ """
1305
+ Fix Mermaid diagram syntax using AI for UI rendering.
1306
+ """
1307
+ try:
1308
+ from helpers.diagram import fix_mermaid_syntax_for_ui
1309
+
1310
+ logger.info(f"[MERMAID] Fixing Mermaid syntax for UI")
1311
+ fixed_code = await fix_mermaid_syntax_for_ui(mermaid_code, error_message)
1312
+
1313
+ return {
1314
+ "success": True,
1315
+ "fixed_code": fixed_code,
1316
+ "was_fixed": fixed_code != mermaid_code
1317
+ }
1318
+
1319
+ except Exception as e:
1320
+ logger.error(f"[MERMAID] Failed to fix Mermaid syntax: {e}")
1321
+ return {
1322
+ "success": False,
1323
+ "error": str(e),
1324
+ "fixed_code": mermaid_code
1325
+ }
1326
+
1327
+
static/script.js CHANGED
@@ -821,33 +821,91 @@
821
  const isV10 = !!(window.mermaid && window.mermaid.render && typeof window.mermaid.render === 'function');
822
  for (let idx = 0; idx < mermaidBlocks.length; idx++) {
823
  const codeBlock = mermaidBlocks[idx];
824
- const graph = codeBlock.textContent || '';
825
  const wrapper = document.createElement('div');
826
  const id = `mermaid-${Date.now()}-${idx}`;
827
  wrapper.className = 'mermaid';
828
  wrapper.id = id;
829
  const replaceTarget = codeBlock.parentElement && codeBlock.parentElement.tagName.toLowerCase() === 'pre' ? codeBlock.parentElement : codeBlock;
830
  replaceTarget.replaceWith(wrapper);
831
- try {
832
- if (isV10) {
833
- // Pass wrapper as container to avoid document.createElementNS undefined errors
834
- const out = await window.mermaid.render(id + '-svg', graph, wrapper);
835
- if (out && out.svg) {
836
- wrapper.innerHTML = out.svg;
837
- if (out.bindFunctions) { out.bindFunctions(wrapper); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
838
  }
839
- } else if (window.mermaid && window.mermaid.init) {
840
- // Legacy fallback
841
- wrapper.textContent = graph;
842
- window.mermaid.init(undefined, wrapper);
843
  }
844
- } catch (e) {
845
- console.warn('Mermaid render failed:', e);
 
 
 
 
846
  wrapper.textContent = graph;
847
  }
848
  }
849
  }
850
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
851
  // Expose markdown-aware appenders for use after refresh (projects.js)
852
  window.appendMessage = appendMessage;
853
  window.appendSources = appendSources;
@@ -873,23 +931,41 @@
873
  const wrapper = document.createElement('div');
874
  wrapper.className = 'code-block-wrapper';
875
 
876
- // Create header with language and copy button
877
  const header = document.createElement('div');
878
  header.className = 'code-block-header';
 
 
 
 
 
879
  header.innerHTML = `
880
  <span class="code-block-language">${language}</span>
881
- <button class="copy-code-btn" data-code-index="${index}">
882
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
883
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
884
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
885
- </svg>
886
- Copy
887
- </button>
 
 
 
 
 
 
 
 
 
 
888
  `;
889
 
890
  // Create content wrapper and move the original <pre><code> inside (preserves highlighting)
891
  const content = document.createElement('div');
892
  content.className = 'code-block-content';
 
 
 
893
  pre.dataset.sbWrapped = '1';
894
  content.appendChild(pre);
895
 
@@ -916,12 +992,42 @@
916
  // If messageDiv contains multiple elements, just append wrapper now
917
  messageDiv.appendChild(wrapper);
918
 
919
- // Add click handler for copy button
920
  const copyBtn = wrapper.querySelector('.copy-code-btn');
921
- copyBtn.addEventListener('click', () => copyCodeToClipboard(codeBlock.textContent, copyBtn));
 
 
 
 
 
 
 
 
922
  });
923
  }
924
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
925
  function copyCodeToClipboard(code, button) {
926
  navigator.clipboard.writeText(code).then(() => {
927
  const originalText = button.innerHTML;
 
821
  const isV10 = !!(window.mermaid && window.mermaid.render && typeof window.mermaid.render === 'function');
822
  for (let idx = 0; idx < mermaidBlocks.length; idx++) {
823
  const codeBlock = mermaidBlocks[idx];
824
+ let graph = codeBlock.textContent || '';
825
  const wrapper = document.createElement('div');
826
  const id = `mermaid-${Date.now()}-${idx}`;
827
  wrapper.className = 'mermaid';
828
  wrapper.id = id;
829
  const replaceTarget = codeBlock.parentElement && codeBlock.parentElement.tagName.toLowerCase() === 'pre' ? codeBlock.parentElement : codeBlock;
830
  replaceTarget.replaceWith(wrapper);
831
+
832
+ // Try to render with retry logic
833
+ let renderSuccess = false;
834
+ let attempt = 0;
835
+ const maxAttempts = 3;
836
+
837
+ while (!renderSuccess && attempt < maxAttempts) {
838
+ try {
839
+ if (isV10) {
840
+ // Pass wrapper as container to avoid document.createElementNS undefined errors
841
+ const out = await window.mermaid.render(id + '-svg', graph, wrapper);
842
+ if (out && out.svg) {
843
+ wrapper.innerHTML = out.svg;
844
+ if (out.bindFunctions) { out.bindFunctions(wrapper); }
845
+ renderSuccess = true;
846
+ }
847
+ } else if (window.mermaid && window.mermaid.init) {
848
+ // Legacy fallback
849
+ wrapper.textContent = graph;
850
+ window.mermaid.init(undefined, wrapper);
851
+ renderSuccess = true;
852
+ }
853
+ } catch (e) {
854
+ console.warn(`Mermaid render failed (attempt ${attempt + 1}):`, e);
855
+
856
+ // If this isn't the last attempt, try to fix the mermaid code using AI
857
+ if (attempt < maxAttempts - 1) {
858
+ try {
859
+ console.log('Attempting to fix Mermaid syntax using AI...');
860
+ const fixedCode = await fixMermaidWithAI(graph, e.message || e.toString());
861
+ if (fixedCode && fixedCode !== graph) {
862
+ graph = fixedCode;
863
+ console.log('AI provided fixed Mermaid code, retrying...');
864
+ } else {
865
+ console.warn('AI could not provide fixed Mermaid code');
866
+ break;
867
+ }
868
+ } catch (aiError) {
869
+ console.warn('AI Mermaid fix failed:', aiError);
870
+ break;
871
+ }
872
  }
 
 
 
 
873
  }
874
+ attempt++;
875
+ }
876
+
877
+ // If all attempts failed, show the original code
878
+ if (!renderSuccess) {
879
+ console.warn('All Mermaid render attempts failed, showing original code');
880
  wrapper.textContent = graph;
881
  }
882
  }
883
  }
884
 
885
+ async function fixMermaidWithAI(mermaidCode, errorMessage) {
886
+ try {
887
+ const formData = new FormData();
888
+ formData.append('mermaid_code', mermaidCode);
889
+ formData.append('error_message', errorMessage);
890
+
891
+ const response = await fetch('/mermaid/fix', {
892
+ method: 'POST',
893
+ body: formData
894
+ });
895
+
896
+ if (response.ok) {
897
+ const result = await response.json();
898
+ if (result.success && result.was_fixed) {
899
+ console.log('Mermaid syntax fixed by AI');
900
+ return result.fixed_code;
901
+ }
902
+ }
903
+ } catch (error) {
904
+ console.warn('Failed to fix Mermaid with AI:', error);
905
+ }
906
+ return null;
907
+ }
908
+
909
  // Expose markdown-aware appenders for use after refresh (projects.js)
910
  window.appendMessage = appendMessage;
911
  window.appendSources = appendSources;
 
931
  const wrapper = document.createElement('div');
932
  wrapper.className = 'code-block-wrapper';
933
 
934
+ // Create header with language and action buttons
935
  const header = document.createElement('div');
936
  header.className = 'code-block-header';
937
+
938
+ // Check if code is long enough to warrant an expand button
939
+ const codeText = codeBlock.textContent || '';
940
+ const isLongCode = codeText.split('\n').length > 15 || codeText.length > 500;
941
+
942
  header.innerHTML = `
943
  <span class="code-block-language">${language}</span>
944
+ <div class="code-block-actions">
945
+ ${isLongCode ? `
946
+ <button class="expand-code-btn" data-code-index="${index}">
947
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
948
+ <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>
949
+ </svg>
950
+ Expand
951
+ </button>
952
+ ` : ''}
953
+ <button class="copy-code-btn" data-code-index="${index}">
954
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
955
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
956
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
957
+ </svg>
958
+ Copy
959
+ </button>
960
+ </div>
961
  `;
962
 
963
  // Create content wrapper and move the original <pre><code> inside (preserves highlighting)
964
  const content = document.createElement('div');
965
  content.className = 'code-block-content';
966
+ if (isLongCode) {
967
+ content.classList.add('collapsed');
968
+ }
969
  pre.dataset.sbWrapped = '1';
970
  content.appendChild(pre);
971
 
 
992
  // If messageDiv contains multiple elements, just append wrapper now
993
  messageDiv.appendChild(wrapper);
994
 
995
+ // Add click handlers for copy and expand buttons
996
  const copyBtn = wrapper.querySelector('.copy-code-btn');
997
+ const expandBtn = wrapper.querySelector('.expand-code-btn');
998
+
999
+ if (copyBtn) {
1000
+ copyBtn.addEventListener('click', () => copyCodeToClipboard(codeBlock.textContent, copyBtn));
1001
+ }
1002
+
1003
+ if (expandBtn) {
1004
+ expandBtn.addEventListener('click', () => toggleCodeExpansion(content, expandBtn));
1005
+ }
1006
  });
1007
  }
1008
 
1009
+ function toggleCodeExpansion(content, button) {
1010
+ const isCollapsed = content.classList.contains('collapsed');
1011
+
1012
+ if (isCollapsed) {
1013
+ content.classList.remove('collapsed');
1014
+ button.innerHTML = `
1015
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1016
+ <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>
1017
+ </svg>
1018
+ Collapse
1019
+ `;
1020
+ } else {
1021
+ content.classList.add('collapsed');
1022
+ button.innerHTML = `
1023
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1024
+ <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>
1025
+ </svg>
1026
+ Expand
1027
+ `;
1028
+ }
1029
+ }
1030
+
1031
  function copyCodeToClipboard(code, button) {
1032
  navigator.clipboard.writeText(code).then(() => {
1033
  const originalText = button.innerHTML;
static/styles.css CHANGED
@@ -1074,57 +1074,82 @@
1074
  overflow: auto;
1075
  }
1076
 
1077
- /* Code blocks with copy button */
1078
  .code-block-wrapper {
1079
  position: relative;
1080
  margin: 20px 0;
1081
  border-radius: 12px;
1082
  overflow: hidden;
1083
  border: 1px solid var(--border);
1084
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
1085
  background: var(--card);
 
 
 
 
 
 
1086
  }
1087
 
1088
  .code-block-header {
1089
  display: flex;
1090
  justify-content: space-between;
1091
  align-items: center;
1092
- padding: 12px 16px;
1093
- background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--card) 100%);
1094
- border-bottom: 1px solid var(--border);
1095
  font-size: 12px;
1096
- color: var(--muted);
1097
  font-weight: 500;
 
 
 
 
 
 
 
 
 
 
 
1098
  }
1099
 
1100
  .code-block-language {
1101
  font-weight: 700;
1102
  text-transform: uppercase;
1103
- letter-spacing: 0.5px;
1104
  color: var(--accent);
1105
  display: flex;
1106
  align-items: center;
1107
- gap: 6px;
 
1108
  }
1109
 
1110
  .code-block-language::before {
1111
  content: '';
1112
- width: 8px;
1113
- height: 8px;
1114
  border-radius: 50%;
1115
  background: var(--accent);
1116
  display: inline-block;
 
1117
  }
1118
 
1119
- .copy-code-btn {
 
 
 
 
 
 
1120
  display: flex;
1121
  align-items: center;
1122
  gap: 6px;
1123
- padding: 6px 12px;
1124
- background: var(--card);
1125
- border: 1px solid var(--border);
1126
  border-radius: 8px;
1127
- color: var(--text-secondary);
1128
  font-size: 11px;
1129
  font-weight: 500;
1130
  cursor: pointer;
@@ -1133,7 +1158,7 @@
1133
  overflow: hidden;
1134
  }
1135
 
1136
- .copy-code-btn::before {
1137
  content: '';
1138
  position: absolute;
1139
  top: 0;
@@ -1144,15 +1169,15 @@
1144
  transition: left 0.5s ease;
1145
  }
1146
 
1147
- .copy-code-btn:hover {
1148
- background: var(--card-hover);
1149
  border-color: var(--accent);
1150
- color: var(--text);
1151
  transform: translateY(-1px);
1152
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1153
  }
1154
 
1155
- .copy-code-btn:hover::before {
1156
  left: 100%;
1157
  }
1158
 
@@ -1163,25 +1188,26 @@
1163
  transform: scale(1.05);
1164
  }
1165
 
1166
- .copy-code-btn svg {
1167
  width: 14px;
1168
  height: 14px;
1169
  transition: transform 0.2s ease;
1170
  }
1171
 
1172
- .copy-code-btn:hover svg {
1173
  transform: scale(1.1);
1174
  }
1175
 
1176
  .code-block-content {
1177
- background: linear-gradient(135deg, #1a1a1a 0%, #1e1e1e 100%);
1178
- color: #e6e6e6;
1179
- padding: 20px;
1180
  overflow-x: auto;
1181
- font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
1182
  font-size: 14px;
1183
- line-height: 1.6;
1184
  position: relative;
 
1185
  }
1186
 
1187
  .code-block-content::before {
@@ -1194,6 +1220,36 @@
1194
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
1195
  }
1196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1197
  /* Enhanced syntax highlighting for code blocks */
1198
  .code-block-content .hljs {
1199
  background: transparent !important;
@@ -1220,12 +1276,75 @@
1220
 
1221
  .code-block-content .hljs-function {
1222
  color: #d2a8ff;
 
1223
  }
1224
 
1225
  .code-block-content .hljs-variable {
1226
  color: #ffa657;
1227
  }
1228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1229
  /* Scrollbar styling for code blocks */
1230
  .code-block-content::-webkit-scrollbar {
1231
  height: 8px;
 
1074
  overflow: auto;
1075
  }
1076
 
1077
+ /* Enhanced IDE-like Code blocks */
1078
  .code-block-wrapper {
1079
  position: relative;
1080
  margin: 20px 0;
1081
  border-radius: 12px;
1082
  overflow: hidden;
1083
  border: 1px solid var(--border);
1084
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
1085
  background: var(--card);
1086
+ transition: all 0.3s ease;
1087
+ }
1088
+
1089
+ .code-block-wrapper:hover {
1090
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
1091
+ transform: translateY(-2px);
1092
  }
1093
 
1094
  .code-block-header {
1095
  display: flex;
1096
  justify-content: space-between;
1097
  align-items: center;
1098
+ padding: 16px 20px;
1099
+ background: linear-gradient(135deg, #2d2d2d 0%, #1e1e1e 100%);
1100
+ border-bottom: 1px solid #404040;
1101
  font-size: 12px;
1102
+ color: #a0a0a0;
1103
  font-weight: 500;
1104
+ position: relative;
1105
+ }
1106
+
1107
+ .code-block-header::before {
1108
+ content: '';
1109
+ position: absolute;
1110
+ top: 0;
1111
+ left: 0;
1112
+ right: 0;
1113
+ height: 1px;
1114
+ background: linear-gradient(90deg, transparent, var(--accent), transparent);
1115
  }
1116
 
1117
  .code-block-language {
1118
  font-weight: 700;
1119
  text-transform: uppercase;
1120
+ letter-spacing: 0.8px;
1121
  color: var(--accent);
1122
  display: flex;
1123
  align-items: center;
1124
+ gap: 8px;
1125
+ font-size: 11px;
1126
  }
1127
 
1128
  .code-block-language::before {
1129
  content: '';
1130
+ width: 10px;
1131
+ height: 10px;
1132
  border-radius: 50%;
1133
  background: var(--accent);
1134
  display: inline-block;
1135
+ box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.3);
1136
  }
1137
 
1138
+ .code-block-actions {
1139
+ display: flex;
1140
+ align-items: center;
1141
+ gap: 8px;
1142
+ }
1143
+
1144
+ .copy-code-btn, .expand-code-btn {
1145
  display: flex;
1146
  align-items: center;
1147
  gap: 6px;
1148
+ padding: 8px 14px;
1149
+ background: rgba(255, 255, 255, 0.05);
1150
+ border: 1px solid rgba(255, 255, 255, 0.1);
1151
  border-radius: 8px;
1152
+ color: #a0a0a0;
1153
  font-size: 11px;
1154
  font-weight: 500;
1155
  cursor: pointer;
 
1158
  overflow: hidden;
1159
  }
1160
 
1161
+ .copy-code-btn::before, .expand-code-btn::before {
1162
  content: '';
1163
  position: absolute;
1164
  top: 0;
 
1169
  transition: left 0.5s ease;
1170
  }
1171
 
1172
+ .copy-code-btn:hover, .expand-code-btn:hover {
1173
+ background: rgba(255, 255, 255, 0.1);
1174
  border-color: var(--accent);
1175
+ color: #ffffff;
1176
  transform: translateY(-1px);
1177
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
1178
  }
1179
 
1180
+ .copy-code-btn:hover::before, .expand-code-btn:hover::before {
1181
  left: 100%;
1182
  }
1183
 
 
1188
  transform: scale(1.05);
1189
  }
1190
 
1191
+ .copy-code-btn svg, .expand-code-btn svg {
1192
  width: 14px;
1193
  height: 14px;
1194
  transition: transform 0.2s ease;
1195
  }
1196
 
1197
+ .copy-code-btn:hover svg, .expand-code-btn:hover svg {
1198
  transform: scale(1.1);
1199
  }
1200
 
1201
  .code-block-content {
1202
+ background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);
1203
+ color: #e6edf3;
1204
+ padding: 24px;
1205
  overflow-x: auto;
1206
+ font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
1207
  font-size: 14px;
1208
+ line-height: 1.7;
1209
  position: relative;
1210
+ border-left: 4px solid var(--accent);
1211
  }
1212
 
1213
  .code-block-content::before {
 
1220
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
1221
  }
1222
 
1223
+ .code-block-content::after {
1224
+ content: '';
1225
+ position: absolute;
1226
+ top: 0;
1227
+ right: 0;
1228
+ width: 1px;
1229
+ height: 100%;
1230
+ background: linear-gradient(180deg, transparent, var(--accent), transparent);
1231
+ }
1232
+
1233
+ /* Line numbers for code blocks */
1234
+ .code-block-content.with-line-numbers {
1235
+ counter-reset: line-number;
1236
+ }
1237
+
1238
+ .code-block-content.with-line-numbers pre {
1239
+ counter-increment: line-number;
1240
+ }
1241
+
1242
+ .code-block-content.with-line-numbers pre::before {
1243
+ content: counter(line-number);
1244
+ position: absolute;
1245
+ left: -40px;
1246
+ width: 30px;
1247
+ text-align: right;
1248
+ color: #6e7681;
1249
+ font-size: 12px;
1250
+ user-select: none;
1251
+ }
1252
+
1253
  /* Enhanced syntax highlighting for code blocks */
1254
  .code-block-content .hljs {
1255
  background: transparent !important;
 
1276
 
1277
  .code-block-content .hljs-function {
1278
  color: #d2a8ff;
1279
+ font-weight: 500;
1280
  }
1281
 
1282
  .code-block-content .hljs-variable {
1283
  color: #ffa657;
1284
  }
1285
 
1286
+ .code-block-content .hljs-built_in {
1287
+ color: #ffa657;
1288
+ font-weight: 500;
1289
+ }
1290
+
1291
+ .code-block-content .hljs-type {
1292
+ color: #d2a8ff;
1293
+ font-weight: 500;
1294
+ }
1295
+
1296
+ .code-block-content .hljs-class {
1297
+ color: #d2a8ff;
1298
+ font-weight: 500;
1299
+ }
1300
+
1301
+ .code-block-content .hljs-title {
1302
+ color: #d2a8ff;
1303
+ font-weight: 500;
1304
+ }
1305
+
1306
+ .code-block-content .hljs-params {
1307
+ color: #e6edf3;
1308
+ }
1309
+
1310
+ .code-block-content .hljs-attr {
1311
+ color: #79c0ff;
1312
+ }
1313
+
1314
+ .code-block-content .hljs-tag {
1315
+ color: #7ee787;
1316
+ }
1317
+
1318
+ .code-block-content .hljs-name {
1319
+ color: #7ee787;
1320
+ }
1321
+
1322
+ .code-block-content .hljs-attribute {
1323
+ color: #79c0ff;
1324
+ }
1325
+
1326
+ .code-block-content .hljs-value {
1327
+ color: #a5d6ff;
1328
+ }
1329
+
1330
+ /* Code folding */
1331
+ .code-block-content.collapsed {
1332
+ max-height: 200px;
1333
+ overflow: hidden;
1334
+ position: relative;
1335
+ }
1336
+
1337
+ .code-block-content.collapsed::after {
1338
+ content: '';
1339
+ position: absolute;
1340
+ bottom: 0;
1341
+ left: 0;
1342
+ right: 0;
1343
+ height: 40px;
1344
+ background: linear-gradient(transparent, #0d1117);
1345
+ pointer-events: none;
1346
+ }
1347
+
1348
  /* Scrollbar styling for code blocks */
1349
  .code-block-content::-webkit-scrollbar {
1350
  height: 8px;
test_improvements.py DELETED
@@ -1,220 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Test script to verify code styling and heading numbering improvements
4
- """
5
- import asyncio
6
- import sys
7
- import os
8
-
9
- # Add the project root to the Python path
10
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
11
-
12
- from utils.service.pdf import _apply_syntax_highlight
13
-
14
- def test_code_styling_improvements():
15
- """Test that the code styling improvements work correctly"""
16
- print("Testing code styling improvements...")
17
-
18
- # Test Python code highlighting
19
- python_code = '''def calculate_fibonacci(n):
20
- """Calculate the nth Fibonacci number"""
21
- if n <= 1:
22
- return n
23
- return calculate_fibonacci(n-1) + calculate_fibonacci(n-2)
24
-
25
- # Test the function
26
- result = calculate_fibonacci(10)
27
- print(f"Fibonacci(10) = {result}")'''
28
-
29
- try:
30
- # Escape the code first
31
- escaped_code = python_code.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
32
- result = _apply_syntax_highlight(escaped_code, 'python')
33
-
34
- print("✓ Python syntax highlighting test passed")
35
- print(" Sample output (first 200 chars):")
36
- print(f" {result[:200]}...")
37
-
38
- # Check for key highlighting elements
39
- assert '<font color=' in result, "Should have color highlighting"
40
- assert 'def' in result, "Should highlight keywords"
41
- assert 'print' in result, "Should highlight built-ins"
42
- assert '#' in result, "Should highlight comments"
43
-
44
- return True
45
- except Exception as e:
46
- print(f"✗ Python syntax highlighting test failed: {e}")
47
- return False
48
-
49
- def test_javascript_highlighting():
50
- """Test JavaScript highlighting"""
51
- print("\nTesting JavaScript highlighting...")
52
-
53
- js_code = '''function processData(data) {
54
- // Process the input data
55
- const result = data.map(item => {
56
- return {
57
- id: item.id,
58
- name: item.name.toUpperCase(),
59
- processed: true
60
- };
61
- });
62
-
63
- return result;
64
- }
65
-
66
- // Usage
67
- const data = [{id: 1, name: "test"}];
68
- const processed = processData(data);
69
- console.log(processed);'''
70
-
71
- try:
72
- escaped_code = js_code.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
73
- result = _apply_syntax_highlight(escaped_code, 'javascript')
74
-
75
- print("✓ JavaScript syntax highlighting test passed")
76
- print(" Sample output (first 200 chars):")
77
- print(f" {result[:200]}...")
78
-
79
- # Check for key highlighting elements
80
- assert '<font color=' in result, "Should have color highlighting"
81
- assert 'function' in result, "Should highlight keywords"
82
- assert 'console' in result, "Should highlight built-ins"
83
-
84
- return True
85
- except Exception as e:
86
- print(f"✗ JavaScript syntax highlighting test failed: {e}")
87
- return False
88
-
89
- def test_json_highlighting():
90
- """Test JSON highlighting"""
91
- print("\nTesting JSON highlighting...")
92
-
93
- json_code = '''{
94
- "name": "test",
95
- "value": 123,
96
- "active": true,
97
- "items": null,
98
- "data": {
99
- "nested": "value"
100
- }
101
- }'''
102
-
103
- try:
104
- escaped_code = json_code.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
105
- result = _apply_syntax_highlight(escaped_code, 'json')
106
-
107
- print("✓ JSON syntax highlighting test passed")
108
- print(" Sample output (first 200 chars):")
109
- print(f" {result[:200]}...")
110
-
111
- # Check for key highlighting elements
112
- assert '<font color=' in result, "Should have color highlighting"
113
- assert 'true' in result, "Should highlight boolean values"
114
- assert 'null' in result, "Should highlight null values"
115
-
116
- return True
117
- except Exception as e:
118
- print(f"✗ JSON syntax highlighting test failed: {e}")
119
- return False
120
-
121
- def test_heading_extraction():
122
- """Test heading extraction logic"""
123
- print("\nTesting heading extraction...")
124
-
125
- sample_report = """# Introduction
126
- This is the introduction section.
127
-
128
- ## Background
129
- Some background information.
130
-
131
- ### Historical Context
132
- Historical details here.
133
-
134
- ## Methodology
135
- Our approach to the problem.
136
-
137
- ### Data Collection
138
- How we collected data.
139
-
140
- ### Analysis
141
- How we analyzed the data.
142
-
143
- ## Results
144
- The findings.
145
-
146
- ## Conclusion
147
- Final thoughts."""
148
-
149
- import re
150
-
151
- # Extract headings using the same logic as the fix_heading_numbering function
152
- heading_pattern = r'^(#{1,6})\s*(.*)$'
153
- headings = []
154
- lines = sample_report.split('\n')
155
-
156
- for i, line in enumerate(lines):
157
- match = re.match(heading_pattern, line.strip())
158
- if match:
159
- level = len(match.group(1))
160
- text = match.group(2).strip()
161
- # Remove existing numbering if present
162
- text = re.sub(r'^\d+\.?\s*', '', text)
163
- headings.append({
164
- 'line_number': i,
165
- 'level': level,
166
- 'text': text,
167
- 'original_line': line
168
- })
169
-
170
- print(f"✓ Extracted {len(headings)} headings:")
171
- for h in headings:
172
- print(f" Level {h['level']}: {h['text']}")
173
-
174
- # Verify we found the expected headings
175
- expected_headings = ['Introduction', 'Background', 'Historical Context', 'Methodology', 'Data Collection', 'Analysis', 'Results', 'Conclusion']
176
- found_texts = [h['text'] for h in headings]
177
-
178
- for expected in expected_headings:
179
- if expected in found_texts:
180
- print(f" ✓ Found: {expected}")
181
- else:
182
- print(f" ✗ Missing: {expected}")
183
-
184
- return len(headings) >= len(expected_headings)
185
-
186
- async def main():
187
- """Run all tests"""
188
- print("Running code styling and heading numbering improvements tests...\n")
189
-
190
- # Test code styling improvements
191
- python_test = test_code_styling_improvements()
192
- js_test = test_javascript_highlighting()
193
- json_test = test_json_highlighting()
194
-
195
- # Test heading extraction
196
- heading_test = test_heading_extraction()
197
-
198
- print("\n" + "="*60)
199
- print("TEST RESULTS:")
200
- print(f"✓ Python highlighting: {'PASSED' if python_test else 'FAILED'}")
201
- print(f"✓ JavaScript highlighting: {'PASSED' if js_test else 'FAILED'}")
202
- print(f"✓ JSON highlighting: {'PASSED' if json_test else 'FAILED'}")
203
- print(f"✓ Heading extraction: {'PASSED' if heading_test else 'FAILED'}")
204
-
205
- all_passed = python_test and js_test and json_test and heading_test
206
-
207
- if all_passed:
208
- print("\n🎉 All tests passed!")
209
- print("✓ Code styling improvements are working correctly")
210
- print("✓ Heading extraction logic is working correctly")
211
- print("✓ The PDF generation should now have better code styling")
212
- print("✓ Reports should have properly numbered headings")
213
- else:
214
- print("\n❌ Some tests failed. Please check the output above.")
215
-
216
- print("\nNote: The heading re-numbering will use AI models during actual report generation.")
217
- print("The extraction logic is tested here, but the AI re-numbering requires a live model.")
218
-
219
- if __name__ == "__main__":
220
- asyncio.run(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/service/pdf.py CHANGED
@@ -13,7 +13,7 @@ from utils.logger import get_logger
13
  logger = get_logger("PDF", __name__)
14
 
15
 
16
- def _parse_markdown_content(content: str, heading1_style, heading2_style, heading3_style, normal_style, code_style):
17
  """
18
  Enhanced markdown parser that properly handles bold/italic formatting
19
  """
@@ -63,11 +63,15 @@ def _parse_markdown_content(content: str, heading1_style, heading2_style, headin
63
  i += 1
64
 
65
  if code_lines:
66
- # Mermaid diagrams → render via Kroki PNG for PDF
67
  if language.lower() == 'mermaid':
68
  try:
69
  from reportlab.platypus import Image, Spacer
70
- img_bytes = _render_mermaid_png('\n'.join(code_lines))
 
 
 
 
71
  if img_bytes and len(img_bytes) > 0:
72
  import io
73
  img = Image(io.BytesIO(img_bytes))
@@ -82,9 +86,9 @@ def _parse_markdown_content(content: str, heading1_style, heading2_style, headin
82
  i += 1
83
  continue
84
  else:
85
- logger.warning("[PDF] Mermaid render returned empty image, falling back to code block")
86
  except Exception as me:
87
- logger.warning(f"[PDF] Mermaid render failed, falling back to code block: {me}")
88
 
89
  # Fallback: render as code block with mermaid syntax
90
  from reportlab.platypus import XPreformatted, Paragraph
@@ -828,12 +832,12 @@ async def generate_report_pdf(report_content: str, user_id: str, project_id: str
828
  story = []
829
 
830
  # Add title
831
- story.append(Paragraph("Study Report", title_style))
832
  story.append(Paragraph(f"<i>Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')}</i>", normal_style))
833
  story.append(Spacer(1, 20))
834
 
835
  # Enhanced markdown parser with proper formatting
836
- story.extend(_parse_markdown_content(report_content, heading1_style, heading2_style, heading3_style, normal_style, code_style))
837
 
838
  # Add references section if sources provided
839
  if sources:
 
13
  logger = get_logger("PDF", __name__)
14
 
15
 
16
+ async def _parse_markdown_content(content: str, heading1_style, heading2_style, heading3_style, normal_style, code_style):
17
  """
18
  Enhanced markdown parser that properly handles bold/italic formatting
19
  """
 
63
  i += 1
64
 
65
  if code_lines:
66
+ # Mermaid diagrams → render via Kroki PNG for PDF with retry logic
67
  if language.lower() == 'mermaid':
68
  try:
69
  from reportlab.platypus import Image, Spacer
70
+ mermaid_code = '\n'.join(code_lines)
71
+ # Use retry logic from diagram.py
72
+ from helpers.diagram import _render_mermaid_with_retry
73
+ img_bytes = await _render_mermaid_with_retry(mermaid_code)
74
+
75
  if img_bytes and len(img_bytes) > 0:
76
  import io
77
  img = Image(io.BytesIO(img_bytes))
 
86
  i += 1
87
  continue
88
  else:
89
+ logger.warning("[PDF] Mermaid render returned empty image after retries, falling back to code block")
90
  except Exception as me:
91
+ logger.warning(f"[PDF] Mermaid render failed after retries, falling back to code block: {me}")
92
 
93
  # Fallback: render as code block with mermaid syntax
94
  from reportlab.platypus import XPreformatted, Paragraph
 
832
  story = []
833
 
834
  # Add title
835
+ story.append(Paragraph("StudyBuddy Report", title_style))
836
  story.append(Paragraph(f"<i>Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')}</i>", normal_style))
837
  story.append(Spacer(1, 20))
838
 
839
  # Enhanced markdown parser with proper formatting
840
+ story.extend(await _parse_markdown_content(report_content, heading1_style, heading2_style, heading3_style, normal_style, code_style))
841
 
842
  # Add references section if sources provided
843
  if sources: