feat: empty states, onboarding checklist, PDF exports, and supporting data #114
@@ -407,18 +407,35 @@ async def export_session(
|
||||
headers={"Content-Disposition": f'attachment; filename="session-export-{session_id}.pdf"'},
|
||||
)
|
||||
|
||||
# Query supporting data for non-PDF formats
|
||||
from app.models.supporting_data import SessionSupportingData
|
||||
sd_result = await db.execute(
|
||||
select(SessionSupportingData)
|
||||
.where(SessionSupportingData.session_id == session_id)
|
||||
.order_by(SessionSupportingData.sort_order)
|
||||
)
|
||||
supporting_data_items = [
|
||||
{
|
||||
"label": sd.label,
|
||||
"data_type": sd.data_type,
|
||||
"content": sd.content,
|
||||
"content_type": sd.content_type,
|
||||
}
|
||||
for sd in sd_result.scalars().all()
|
||||
]
|
||||
|
||||
# Generate export based on format
|
||||
if export_options.format == "markdown":
|
||||
content = generate_markdown_export(session, export_options)
|
||||
content = generate_markdown_export(session, export_options, supporting_data=supporting_data_items)
|
||||
media_type = "text/markdown"
|
||||
elif export_options.format == "html":
|
||||
content = generate_html_export(session, export_options)
|
||||
content = generate_html_export(session, export_options, supporting_data=supporting_data_items)
|
||||
media_type = "text/html"
|
||||
elif export_options.format == "psa":
|
||||
content = generate_psa_export(session, export_options)
|
||||
content = generate_psa_export(session, export_options, supporting_data=supporting_data_items)
|
||||
media_type = "text/plain"
|
||||
else: # text
|
||||
content = generate_text_export(session, export_options)
|
||||
content = generate_text_export(session, export_options, supporting_data=supporting_data_items)
|
||||
media_type = "text/plain"
|
||||
|
||||
# Resolve variables in export output
|
||||
|
||||
@@ -171,7 +171,7 @@ def _escape_markdown_table(value: str) -> str:
|
||||
return value.replace("|", "\\|").replace("\n", " ")
|
||||
|
||||
|
||||
def generate_markdown_export(session: Session, options: SessionExport) -> str:
|
||||
def generate_markdown_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None) -> str:
|
||||
"""Generate markdown export."""
|
||||
if _is_procedural_session(session):
|
||||
return _generate_procedural_markdown(session, options)
|
||||
@@ -261,6 +261,22 @@ def generate_markdown_export(session: Session, options: SessionExport) -> str:
|
||||
lines.append(f"*{decision['timestamp']}*")
|
||||
lines.append("")
|
||||
|
||||
# Supporting Data
|
||||
if supporting_data:
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append("## Supporting Data")
|
||||
lines.append("")
|
||||
for sd in supporting_data:
|
||||
lines.append(f"### {sd['label']}")
|
||||
if sd["data_type"] == "text_snippet":
|
||||
lines.append("```")
|
||||
lines.append(sd["content"])
|
||||
lines.append("```")
|
||||
else:
|
||||
lines.append(f"[Screenshot: {sd['label']}]")
|
||||
lines.append("")
|
||||
|
||||
# Resolution / Outcome Notes
|
||||
_raw_notes = getattr(session, 'outcome_notes', None)
|
||||
outcome_notes = _raw_notes if isinstance(_raw_notes, str) else ''
|
||||
@@ -286,7 +302,7 @@ def generate_markdown_export(session: Session, options: SessionExport) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_text_export(session: Session, options: SessionExport) -> str:
|
||||
def generate_text_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None) -> str:
|
||||
"""Generate plain text export."""
|
||||
if _is_procedural_session(session):
|
||||
return _generate_procedural_text(session, options)
|
||||
@@ -361,6 +377,19 @@ def generate_text_export(session: Session, options: SessionExport) -> str:
|
||||
if duration_seconds is not None:
|
||||
lines.append(f" Duration: {_format_step_duration(duration_seconds)}")
|
||||
|
||||
# Supporting Data
|
||||
if supporting_data:
|
||||
lines.append("")
|
||||
lines.append("SUPPORTING DATA")
|
||||
lines.append("-" * 20)
|
||||
for sd in supporting_data:
|
||||
lines.append(f"\n {sd['label']}:")
|
||||
if sd["data_type"] == "text_snippet":
|
||||
for content_line in sd["content"].splitlines():
|
||||
lines.append(f" {content_line}")
|
||||
else:
|
||||
lines.append(f" [Screenshot: {sd['label']}]")
|
||||
|
||||
# Resolution
|
||||
_raw_notes = getattr(session, 'outcome_notes', None)
|
||||
outcome_notes = _raw_notes if isinstance(_raw_notes, str) else ''
|
||||
@@ -382,7 +411,7 @@ def generate_text_export(session: Session, options: SessionExport) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_html_export(session: Session, options: SessionExport) -> str:
|
||||
def generate_html_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None) -> str:
|
||||
"""Generate HTML export."""
|
||||
if _is_procedural_session(session):
|
||||
return _generate_procedural_html(session, options)
|
||||
@@ -472,6 +501,15 @@ def generate_html_export(session: Session, options: SessionExport) -> str:
|
||||
html_parts.append(f'<p class="timestamp">{html.escape(str(decision["timestamp"]))}</p>')
|
||||
html_parts.append('</div>')
|
||||
|
||||
# Supporting Data
|
||||
if supporting_data:
|
||||
html_parts.append('<h2>Supporting Data</h2>')
|
||||
for sd in supporting_data:
|
||||
if sd["data_type"] == "text_snippet":
|
||||
html_parts.append(f'<div class="supporting-item"><h3>{html.escape(sd["label"])}</h3><pre>{html.escape(sd["content"])}</pre></div>')
|
||||
else:
|
||||
html_parts.append(f'<div class="supporting-item"><h3>{html.escape(sd["label"])}</h3><img src="data:{html.escape(sd.get("content_type") or "image/png")};base64,{sd["content"]}" alt="{html.escape(sd["label"])}"></div>')
|
||||
|
||||
# Resolution
|
||||
_raw_notes = getattr(session, 'outcome_notes', None)
|
||||
outcome_notes = _raw_notes if isinstance(_raw_notes, str) else ''
|
||||
@@ -490,7 +528,7 @@ def generate_html_export(session: Session, options: SessionExport) -> str:
|
||||
return "\n".join(html_parts)
|
||||
|
||||
|
||||
def generate_psa_export(session: Session, options: SessionExport) -> str:
|
||||
def generate_psa_export(session: Session, options: SessionExport, supporting_data: list[dict] | None = None) -> str:
|
||||
"""Generate PSA/ticket note export optimized for ConnectWise and similar PSA tools."""
|
||||
if _is_procedural_session(session):
|
||||
return _generate_procedural_psa(session, options)
|
||||
@@ -561,6 +599,19 @@ def generate_psa_export(session: Session, options: SessionExport) -> str:
|
||||
lines.append("No steps recorded.")
|
||||
lines.append("")
|
||||
|
||||
# Supporting Data
|
||||
if supporting_data:
|
||||
lines.append("--- SUPPORTING DATA ---")
|
||||
for sd in supporting_data:
|
||||
if sd["data_type"] == "text_snippet":
|
||||
lines.append(f"## {sd['label']}")
|
||||
lines.append("```")
|
||||
lines.append(sd["content"])
|
||||
lines.append("```")
|
||||
else:
|
||||
lines.append(f"[Screenshot: {sd['label']}]")
|
||||
lines.append("")
|
||||
|
||||
# Resolution — only for completed sessions
|
||||
if session.completed_at:
|
||||
lines.append("--- RESOLUTION ---")
|
||||
|
||||
Reference in New Issue
Block a user