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"'},
|
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
|
# Generate export based on format
|
||||||
if export_options.format == "markdown":
|
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"
|
media_type = "text/markdown"
|
||||||
elif export_options.format == "html":
|
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"
|
media_type = "text/html"
|
||||||
elif export_options.format == "psa":
|
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"
|
media_type = "text/plain"
|
||||||
else: # text
|
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"
|
media_type = "text/plain"
|
||||||
|
|
||||||
# Resolve variables in export output
|
# Resolve variables in export output
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ def _escape_markdown_table(value: str) -> str:
|
|||||||
return value.replace("|", "\\|").replace("\n", " ")
|
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."""
|
"""Generate markdown export."""
|
||||||
if _is_procedural_session(session):
|
if _is_procedural_session(session):
|
||||||
return _generate_procedural_markdown(session, options)
|
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(f"*{decision['timestamp']}*")
|
||||||
lines.append("")
|
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
|
# Resolution / Outcome Notes
|
||||||
_raw_notes = getattr(session, 'outcome_notes', None)
|
_raw_notes = getattr(session, 'outcome_notes', None)
|
||||||
outcome_notes = _raw_notes if isinstance(_raw_notes, str) else ''
|
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)
|
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."""
|
"""Generate plain text export."""
|
||||||
if _is_procedural_session(session):
|
if _is_procedural_session(session):
|
||||||
return _generate_procedural_text(session, options)
|
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:
|
if duration_seconds is not None:
|
||||||
lines.append(f" Duration: {_format_step_duration(duration_seconds)}")
|
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
|
# Resolution
|
||||||
_raw_notes = getattr(session, 'outcome_notes', None)
|
_raw_notes = getattr(session, 'outcome_notes', None)
|
||||||
outcome_notes = _raw_notes if isinstance(_raw_notes, str) else ''
|
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)
|
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."""
|
"""Generate HTML export."""
|
||||||
if _is_procedural_session(session):
|
if _is_procedural_session(session):
|
||||||
return _generate_procedural_html(session, options)
|
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(f'<p class="timestamp">{html.escape(str(decision["timestamp"]))}</p>')
|
||||||
html_parts.append('</div>')
|
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
|
# Resolution
|
||||||
_raw_notes = getattr(session, 'outcome_notes', None)
|
_raw_notes = getattr(session, 'outcome_notes', None)
|
||||||
outcome_notes = _raw_notes if isinstance(_raw_notes, str) else ''
|
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)
|
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."""
|
"""Generate PSA/ticket note export optimized for ConnectWise and similar PSA tools."""
|
||||||
if _is_procedural_session(session):
|
if _is_procedural_session(session):
|
||||||
return _generate_procedural_psa(session, options)
|
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("No steps recorded.")
|
||||||
lines.append("")
|
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
|
# Resolution — only for completed sessions
|
||||||
if session.completed_at:
|
if session.completed_at:
|
||||||
lines.append("--- RESOLUTION ---")
|
lines.append("--- RESOLUTION ---")
|
||||||
|
|||||||
Reference in New Issue
Block a user