study-interface / ui /formatting.py
Rachel Kim
first commit
0793785
Raw History Blame Contribute Delete
10.2 kB
import markdown
from pygments.formatters import HtmlFormatter
import re
import hashlib
# def format_latex(text):
# """
# Convert a Markdown string containing LaTeX formulas to HTML with MathML.
# Handles:
# - Inline math: $...$ or \( ... \)
# - Display math: $$...$$ or \[ ... \]
# Automatically converts `aligned` to `array` and wraps display formulas
# in <span style="display:block;"> for spacing without breaking Markdown lists.
# Args:
# text (str): Markdown string with LaTeX.
# convert (callable): Function that converts LaTeX string to MathML.
# Returns:
# str: HTML string with MathML replacing LaTeX.
# """
# # Pattern for LaTeX delimiters
# pattern = r'(\$\$.*?\$\$|\\\[.*?\\\]|\\\(.*?\\\)|\$.*?\$)'
# def replace_latex(match):
# latex_text = match.group()
# # Determine delimiter type
# is_display = False
# if latex_text.startswith('$$') and latex_text.endswith('$$'):
# latex_text_clean = latex_text[2:-2].strip()
# is_display = True
# elif latex_text.startswith(r'\[') and latex_text.endswith(r'\]'):
# latex_text_clean = latex_text[2:-2].strip()
# is_display = True
# elif latex_text.startswith(r'\(') and latex_text.endswith(r'\)'):
# latex_text_clean = latex_text[2:-2].strip()
# elif latex_text.startswith('$') and latex_text.endswith('$'):
# latex_text_clean = latex_text[1:-1].strip()
# else:
# latex_text_clean = latex_text.strip()
# # Replace aligned -> array
# latex_text_clean = re.sub(
# r'\\begin\{aligned\}', r'\\begin{array}{l}', latex_text_clean
# )
# latex_text_clean = re.sub(
# r'\\end\{aligned\}', r'\\end{array}', latex_text_clean
# )
# # Convert to MathML
# try:
# mathml = convert(latex_text_clean)
# if is_display:
# # Use span with display:block to preserve Markdown list numbering
# mathml = f'<span style="display:block; margin:10px 0;">{mathml}</span>'
# return mathml
# except Exception as e:
# print(f"Warning: failed to convert LaTeX: {latex_text_clean}\nError: {e}")
# return latex_text_clean
# # Replace all LaTeX with MathML
# html_with_mathml = re.sub(pattern, replace_latex, text, flags=re.DOTALL)
# return html_with_mathml
def normalize_list_formatting(text):
# Normalize indentation for nested lists and add spacing
lines = text.split('\n')
result = []
for i, line in enumerate(lines):
# Skip empty lines
if not line.strip():
result.append(line)
continue
# Check if this is a top-level list item (no leading spaces)
is_top_level_list = re.match(r'^(\d+\.|[-*])\s+', line)
# Check if this is a nested list item (indented with any amount of spaces)
nested_match = re.match(r'^( +)([-*]|\d+\.)\s+', line)
# Add blank line before top-level list items if previous line exists and isn't blank
if is_top_level_list and i > 0 and lines[i-1].strip() != '':
result.append('')
# Normalize nested list indentation to 2 spaces per level
if nested_match:
indent_count = len(nested_match.group(1))
# Convert any indentation to proper 2-space indentation
level = max(1, (indent_count + 1) // 2) # Calculate nesting level
normalized_indent = ' ' * level # Use 2 spaces per level
content = line.lstrip()
result.append(normalized_indent + content)
else:
result.append(line)
return '\n'.join(result)
def format_history(history):
"""Format conversation history as HTML with side-by-side responses."""
# Generate Pygments CSS for BOTH light and dark themes
light_css = HtmlFormatter(style='default').get_style_defs('.codehilite')
dark_css = HtmlFormatter(style='monokai').get_style_defs('.codehilite')
html = f'''
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;">
<style>
/* Light theme syntax highlighting (default) */
{light_css}
.response-content pre {{
background-color: #f5f5f5;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
margin: 10px 0;
}}
.response-content code {{
background-color: #f0f0f0;
color: #c7254e;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
white-space: pre-wrap;
word-wrap: break-word;
}}
.response-content pre code {{
background-color: transparent;
color: inherit;
padding: 0;
}}
/* Dark theme overrides */
@media (prefers-color-scheme: dark) {{
{dark_css}
.response-content pre {{
background-color: #272822;
}}
.response-content code {{
background-color: #2d2d2d;
color: #e8e8e8;
}}
.response-content pre code {{
background-color: transparent;
color: inherit;
}}
}}
.response-content table {{
border-collapse: collapse;
width: 100%;
margin: 10px 0;
}}
.response-content th, .response-content td {{
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}}
.response-content th {{
background-color: #f0f0f0;
}}
@media (prefers-color-scheme: dark) {{
.response-content th, .response-content td {{
border-color: #444;
}}
.response-content th {{
background-color: #333;
}}
}}
</style>'''
for turn in history:
# User message - escape HTML but preserve line breaks
user_msg = turn["user"].replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
html += f'<div style="background-color: var(--color-accent-soft); padding: 10px; margin: 10px 0; border-radius: 5px; overflow-y: auto; flex: 1; max-height: 400px;"><strong>You:</strong> {user_msg}</div>'
# Determine opacity based on highlight
opacity_a = "1.0"
opacity_b = "1.0"
border_a = "2px solid var(--block-border-color)"
border_b = "2px solid var(--block-border-color)"
if turn["highlight"] == "left":
opacity_b = "0.3"
border_a = "3px solid #4CAF50"
elif turn["highlight"] == "right":
opacity_a = "0.3"
border_b = "3px solid #4CAF50"
elif turn["highlight"] == "random_left_tie" or turn["highlight"] == "random_left_bad":
opacity_b = "0.3"
border_a = "3px solid #4C50AF"
elif turn["highlight"] == "random_right_tie" or turn["highlight"] == "random_right_bad":
opacity_a = "0.3"
border_b = "3px solid #4C50AF"
normalized_a = normalize_list_formatting(turn["response_a"])
response_a_html = markdown.markdown(
normalized_a,
extensions=['fenced_code', 'tables', 'codehilite', 'sane_lists'],
extension_configs={'codehilite': {'guess_lang': True, 'linenums': False}}
)
normalized_b = normalize_list_formatting(turn["response_b"])
response_b_html = markdown.markdown(
normalized_b,
extensions=['fenced_code', 'tables', 'codehilite', 'sane_lists'],
extension_configs={'codehilite': {'guess_lang': True, 'linenums': False}}
)
# Responses side by side
html += f'''
<div style="display: flex; gap: 10px; margin: 10px auto; align-items: stretch; max-width: 100%; padding: 0 10px; box-sizing: border-box;">
<div style="flex: 1 1 0; min-width: 0; max-width: 50%; background-color: var(--background-fill-secondary); padding: 15px; border-radius: 5px; border: {border_a}; opacity: {opacity_a}; display: flex; flex-direction: column;">
<div style="margin-bottom: 8px;"><strong>Assistant A</strong></div>
<div class="response-content" style="line-height: 1.6; overflow-x: auto; overflow-y: auto; flex: 1; max-height: 400px;">{response_a_html}</div>
</div>
<div style="flex: 1 1 0; min-width: 0; max-width: 50%; background-color: var(--background-fill-secondary); padding: 15px; border-radius: 5px; border: {border_b}; opacity: {opacity_b}; display: flex; flex-direction: column;">
<div style="margin-bottom: 8px;"><strong>Assistant B</strong></div>
<div class="response-content" style="line-height: 1.6; overflow-x: auto; overflow-y: auto; flex: 1; max-height: 400px;">{response_b_html}</div>
</div>
</div>
'''
if turn["highlight"] == "random_left_tie":
html += '<div>(<b>Note:</b> you did not specify a preference for either assistant, so in subsequent turns, Assistant A will be used as context).</div>'
elif turn["highlight"] == "random_right_tie":
html += '<div>(<b>Note:</b> you did not specify a preference for either assistant, so in subsequent turns, Assistant B will be used as context).</div>'
elif turn["highlight"] == "random_left_bad":
html += '<div>(<b>Note:</b> even though you thought both assistants were bad, Assistant A will be used as context in subsequent turns).</div>'
elif turn["highlight"] == "random_right_bad":
html += '<div>(<b>Note:</b> even though you though both assistants were bad, Assistant B will be used as context in subsequent turns).</div>'
html += '</div>'
return html