feat: empty states, onboarding checklist, PDF exports, and supporting data #114

Merged
chihlasm merged 16 commits from feat/backend-foundation-empty-states-exports into main 2026-03-18 00:42:30 +00:00
2 changed files with 76 additions and 8 deletions
Showing only changes of commit f7271e22ae - Show all commits

View File

@@ -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

View File

@@ -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 ---")