MSG
Feat/fix stuff and space basics (#13)
196a48f
Raw
History Blame Contribute Delete
6.63 kB
from __future__ import annotations
import html
import json
from typing import Any
def _icon_for_source(source_type: str) -> str:
st = (source_type or "").lower()
if st in ("web", "url", "scrape"):
return "language"
if st == "pdf":
return "picture_as_pdf"
return "description"
def render_doc_cards(documents: list[dict[str, Any]], *, rag_active: bool) -> str:
if not documents:
return (
'<p class="studio-empty-docs">No documents indexed yet. Paste a URL or upload a file.</p>'
)
badge = (
'<span class="studio-badge studio-badge-rag">RAG Active</span>'
if rag_active
else '<span class="studio-badge studio-badge-muted">No sources</span>'
)
cards: list[str] = []
for doc in documents:
title = html.escape(str(doc.get("title") or "Untitled"))
meta = html.escape(str(doc.get("meta") or ""))
icon = _icon_for_source(str(doc.get("source_type") or ""))
cards.append(
f"""
<div class="studio-doc-card" data-doc-id="{html.escape(str(doc.get("id", "")))}">
<span class="material-symbols-outlined studio-doc-icon">{icon}</span>
<div class="studio-doc-body">
<p class="studio-doc-title">{title}</p>
<p class="studio-doc-meta">{meta}</p>
</div>
</div>"""
)
return f'<div class="studio-doc-header">{badge}</div><div class="studio-doc-list">{"".join(cards)}</div>'
def render_slide_canvas(preview_html: str, *, empty_message: str | None = None) -> str:
if not preview_html or not preview_html.strip():
msg = html.escape(empty_message or "Generate slides to preview your lesson here.")
return f'<div class="studio-canvas-empty"><p>{msg}</p></div>'
return f'<div class="studio-canvas-inner">{preview_html}</div>'
def render_gallery_strip(image_paths: list[str]) -> str:
if not image_paths:
return ""
items: list[str] = []
for index, path in enumerate(image_paths):
if not path:
continue
src = html.escape(f"/file={path}")
alt = html.escape(f"Slide {index + 1}")
items.append(
f'<a class="studio-gallery-item" href="{src}" target="_blank" rel="noopener">'
f'<img src="{src}" alt="{alt}" loading="lazy" /></a>'
)
if not items:
return ""
return f'<div class="studio-gallery-strip">{"".join(items)}</div>'
def render_trace_details(
*,
trace_summary: str = "",
trace_json: str = "",
progress_log: str = "",
) -> str:
blocks: list[str] = []
if trace_summary:
safe = html.escape(trace_summary)
blocks.append(f'<pre class="studio-trace-summary">{safe}</pre>')
if progress_log:
if "<" in progress_log and ">" in progress_log:
blocks.append(f'<div class="studio-trace-log">{progress_log}</div>')
else:
blocks.append(
f'<pre class="studio-trace-log">{html.escape(progress_log)}</pre>'
)
if trace_json:
try:
parsed = json.loads(trace_json)
pretty = html.escape(json.dumps(parsed, indent=2))
except json.JSONDecodeError:
pretty = html.escape(trace_json)
blocks.append(f'<pre class="studio-trace-json">{pretty}</pre>')
if not blocks:
return ""
return f'<div class="studio-trace-details">{"".join(blocks)}</div>'
def _file_url(path: str | None) -> str:
if not path:
return ""
return html.escape(f"/file={path}")
def render_echo_coach_panel(
*,
pace_score: int | None = None,
wpm: float | None = None,
tip: str | None = None,
report_md: str | None = None,
transcript_html: str | None = None,
filler_chart: str | None = None,
pace_chart: str | None = None,
voiceout_path: str | None = None,
listening: bool = False,
) -> str:
if listening:
return """
<div class="studio-coach-panel studio-coach-live">
<div class="studio-coach-header">
<span class="studio-coach-dot"></span>
<span class="studio-coach-label">Recording…</span>
</div>
<p class="studio-coach-hint">Speak your lesson, then analyze for pace and filler feedback.</p>
</div>"""
if pace_score is None and not tip and not report_md and not transcript_html:
return """
<div class="studio-coach-panel studio-coach-idle">
<p class="studio-coach-hint">Record a pitch below, then click <strong>Analyze pitch</strong> for metrics.</p>
</div>"""
score = pace_score if pace_score is not None else "—"
pace = f"{wpm:.0f}" if wpm is not None else "—"
tip_html = html.escape(tip or "")
report_block = ""
if report_md:
safe = html.escape(report_md[:1200])
report_block = f'<div class="studio-coach-report">{safe}</div>'
transcript_block = ""
if transcript_html:
transcript_block = f'<div class="studio-coach-transcript">{transcript_html}</div>'
charts: list[str] = []
filler_url = _file_url(filler_chart)
pace_url = _file_url(pace_chart)
if filler_url:
charts.append(
f'<figure class="studio-coach-chart"><figcaption>Filler words</figcaption>'
f'<img src="{filler_url}" alt="Filler words chart" /></figure>'
)
if pace_url:
charts.append(
f'<figure class="studio-coach-chart"><figcaption>Pace timeline</figcaption>'
f'<img src="{pace_url}" alt="Pace timeline chart" /></figure>'
)
charts_block = ""
if charts:
charts_block = f'<div class="studio-coach-charts">{"".join(charts)}</div>'
voiceout_block = ""
voiceout_url = _file_url(voiceout_path)
if voiceout_url:
voiceout_block = (
f'<div class="studio-coach-voiceout">'
f'<p class="studio-coach-voiceout-label">VoiceOut feedback</p>'
f'<audio controls src="{voiceout_url}"></audio></div>'
)
return f"""
<div class="studio-coach-panel studio-coach-results">
<div class="studio-coach-header">
<span class="studio-coach-label">Analysis results</span>
<span class="studio-coach-tag">EchoCoach</span>
</div>
<div class="studio-coach-metrics">
<div class="studio-metric">
<p class="studio-metric-label">Pace score</p>
<p class="studio-metric-value">{score}</p>
</div>
<div class="studio-metric">
<p class="studio-metric-label">Pace (WPM)</p>
<p class="studio-metric-value">{pace}</p>
</div>
</div>
{f'<p class="studio-coach-tip">{tip_html}</p>' if tip_html else ''}
{transcript_block}
{charts_block}
{voiceout_block}
{report_block}
</div>"""