import math LEGEND_HTML = """
Confidence:  Low (0%)  Moderate (50%)  High (100%)   Hover over tokens/phrases to see values.
""" TOOLTIP_CSS = """ span[data-tooltip] { position: relative; } span[data-tooltip]:hover::after { content: attr(data-tooltip); position: absolute; bottom: 130%; left: 50%; transform: translateX(-50%); background: #333; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.75em; white-space: nowrap; z-index: 9999; pointer-events: none; } """ def hex_to_rgb(hex_code): r = int(hex_code[0:2], 16) g = int(hex_code[2:4], 16) b = int(hex_code[4:6], 16) return r, g, b def prob_to_color(avg_prob): p = max(0.0, min(1.0, float(avg_prob))) r = int((1 - p) * 255) g = int(p * 255) return f"{r:02x}{g:02x}00" def _span(text, color_hex, tooltip): r, g, b = hex_to_rgb(color_hex) brightness = (r * 299 + g * 587 + b * 114) / 1000 text_color = "black" if brightness > 128 else "white" safe_text = text.replace("&", "&").replace("<", "<").replace(">", ">").replace(" ", " ") return ( f'' f'{safe_text}' ) def _wrap(inner_html): return f"

{inner_html}

" def _token_span(text, color_hex, tooltip): """Inline highlight (no pill padding/margin) so tokens flow as continuous text.""" r, g, b = hex_to_rgb(color_hex) brightness = (r * 299 + g * 587 + b * 114) / 1000 text_color = "black" if brightness > 128 else "white" safe_text = text.replace("&", "&").replace("<", "<").replace(">", ">") return ( f'' f'{safe_text}' ) def _wrap_flow(inner_html): """Box matching the semantic-entropy view: words flow and wrap inside it.""" return ( "
" f"{inner_html}
" ) def render_token_probs_html(token_logprobs): """token_logprobs: list of {"token": str, "logprob": float}""" if not token_logprobs: return "

No token probability data available.

" spans = "" for item in token_logprobs: prob = math.exp(item["logprob"]) color = prob_to_color(prob) tooltip = f"p={prob:.3f} logprob={item['logprob']:.3f}" spans += _token_span(item["token"], color, tooltip) return _wrap_flow(spans) def color_code_answer_html(answer_text, sp_to_entropy, sentence_parts): """Render answer with sentence-part spans colored by semantic-entropy confidence.""" if not sentence_parts or not sp_to_entropy or not answer_text: safe = answer_text.replace("&", "&").replace("<", "<").replace(">", ">") return f"

{safe}

" # Find each sentence_part as a substring and record its position colored_parts = [] for sp in sentence_parts: if not sp: continue idx = answer_text.find(sp) if idx == -1: continue entropy = sp_to_entropy.get(sp, 0.0) confidence = math.exp(-entropy) color = prob_to_color(confidence) tooltip = f"entropy={entropy:.3f} confidence={confidence:.3f}" colored_parts.append((idx, idx + len(sp), sp, color, tooltip)) colored_parts.sort(key=lambda x: x[0]) html = "" pos = 0 for start, end, text, color, tooltip in colored_parts: if start > pos: gap = answer_text[pos:start] html += gap.replace("&", "&").replace("<", "<").replace(">", ">") html += _span(text, color, tooltip) pos = end if pos < len(answer_text): tail = answer_text[pos:] html += tail.replace("&", "&").replace("<", "<").replace(">", ">") return _wrap(html)