feat: include supporting data in all export formats
Query supporting data in the export endpoint and pass to markdown, text, HTML, and PSA export generators. Each format renders text snippets and screenshot placeholders in its native style. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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