diff --git a/surfsense_backend/alembic/versions/100_add_report_group_id.py b/surfsense_backend/alembic/versions/100_add_report_group_id.py index cfcbb0635..e99ab0c83 100644 --- a/surfsense_backend/alembic/versions/100_add_report_group_id.py +++ b/surfsense_backend/alembic/versions/100_add_report_group_id.py @@ -67,4 +67,3 @@ def downgrade() -> None: END $$; """ ) - diff --git a/surfsense_backend/alembic/versions/99_add_reports_table.py b/surfsense_backend/alembic/versions/99_add_reports_table.py index 959693251..663e6869b 100644 --- a/surfsense_backend/alembic/versions/99_add_reports_table.py +++ b/surfsense_backend/alembic/versions/99_add_reports_table.py @@ -64,4 +64,3 @@ def downgrade() -> None: op.execute("DROP INDEX IF EXISTS ix_reports_thread_id") op.execute("DROP INDEX IF EXISTS ix_reports_search_space_id") op.execute("DROP TABLE IF EXISTS reports") - diff --git a/surfsense_backend/app/agents/new_chat/tools/report.py b/surfsense_backend/app/agents/new_chat/tools/report.py index 04ee27ef0..c5a8af929 100644 --- a/surfsense_backend/app/agents/new_chat/tools/report.py +++ b/surfsense_backend/app/agents/new_chat/tools/report.py @@ -212,14 +212,18 @@ def create_generate_report_tool( ) return failed_report.id except Exception: - logger.exception("[generate_report] Could not persist failed report row") + logger.exception( + "[generate_report] Could not persist failed report row" + ) return None try: # Get the LLM instance for this search space llm = await get_document_summary_llm(db_session, search_space_id) if not llm: - error_msg = "No LLM configured. Please configure a language model in Settings." + error_msg = ( + "No LLM configured. Please configure a language model in Settings." + ) report_id = await _save_failed_report(error_msg) return { "status": "failed", diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index 864eb7980..e232f0e14 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -1039,7 +1039,9 @@ class Report(BaseModel, TimestampMixin): title = Column(String(500), nullable=False) content = Column(Text, nullable=True) # Markdown body report_metadata = Column(JSONB, nullable=True) # section headings, word count, etc. - report_style = Column(String(100), nullable=True) # e.g. "executive_summary", "deep_research" + report_style = Column( + String(100), nullable=True + ) # e.g. "executive_summary", "deep_research" search_space_id = Column( Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index 3e949c687..db4e81026 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -32,9 +32,9 @@ from .notes_routes import router as notes_router from .notifications_routes import router as notifications_router from .notion_add_connector_route import router as notion_add_connector_router from .podcasts_routes import router as podcasts_router -from .reports_routes import router as reports_router from .public_chat_routes import router as public_chat_router from .rbac_routes import router as rbac_router +from .reports_routes import router as reports_router from .search_source_connectors_routes import router as search_source_connectors_router from .search_spaces_routes import router as search_spaces_router from .slack_add_connector_route import router as slack_add_connector_router diff --git a/surfsense_backend/app/routes/reports_routes.py b/surfsense_backend/app/routes/reports_routes.py index 39ff320f5..08c8e040b 100644 --- a/surfsense_backend/app/routes/reports_routes.py +++ b/surfsense_backend/app/routes/reports_routes.py @@ -51,6 +51,7 @@ class ExportFormat(str, Enum): # Helpers # --------------------------------------------------------------------------- + async def _get_report_with_access( report_id: int, session: AsyncSession, @@ -66,7 +67,7 @@ async def _get_report_with_access( if not report: raise HTTPException(status_code=404, detail="Report not found") - # Lightweight membership check – no granular RBAC, just "is the user a + # Lightweight membership check - no granular RBAC, just "is the user a # member of the search space this report belongs to?" await check_search_space_access(session, user, report.search_space_id) @@ -191,7 +192,9 @@ async def read_report_content( @router.get("/reports/{report_id}/export") async def export_report( report_id: int, - format: ExportFormat = Query(ExportFormat.PDF, description="Export format: pdf or docx"), + format: ExportFormat = Query( + ExportFormat.PDF, description="Export format: pdf or docx" + ), session: AsyncSession = Depends(get_async_session), user: User = Depends(current_active_user), ): @@ -243,8 +246,9 @@ async def export_report( # Sanitize filename safe_title = ( - "".join(c if c.isalnum() or c in " -_" else "_" for c in report.title) - .strip()[:80] + "".join( + c if c.isalnum() or c in " -_" else "_" for c in report.title + ).strip()[:80] or "report" ) @@ -265,9 +269,7 @@ async def export_report( raise except Exception as e: logger.exception("Report export failed") - raise HTTPException( - status_code=500, detail=f"Export failed: {e!s}" - ) from e + raise HTTPException(status_code=500, detail=f"Export failed: {e!s}") from e @router.delete("/reports/{report_id}", response_model=dict) diff --git a/surfsense_backend/app/schemas/__init__.py b/surfsense_backend/app/schemas/__init__.py index 05c069a01..e3f03b29c 100644 --- a/surfsense_backend/app/schemas/__init__.py +++ b/surfsense_backend/app/schemas/__init__.py @@ -59,7 +59,6 @@ from .new_llm_config import ( NewLLMConfigUpdate, ) from .podcasts import PodcastBase, PodcastCreate, PodcastRead, PodcastUpdate -from .reports import ReportBase, ReportContentRead, ReportRead, ReportVersionInfo from .rbac_schemas import ( InviteAcceptRequest, InviteAcceptResponse, @@ -77,6 +76,7 @@ from .rbac_schemas import ( RoleUpdate, UserSearchSpaceAccess, ) +from .reports import ReportBase, ReportContentRead, ReportRead, ReportVersionInfo from .search_source_connector import ( MCPConnectorCreate, MCPConnectorRead, diff --git a/surfsense_backend/app/services/public_chat_service.py b/surfsense_backend/app/services/public_chat_service.py index 9c4ed0c7c..a74d45991 100644 --- a/surfsense_backend/app/services/public_chat_service.py +++ b/surfsense_backend/app/services/public_chat_service.py @@ -229,9 +229,7 @@ async def create_snapshot( result_data = part.get("result", {}) report_id = result_data.get("report_id") if report_id and report_id not in report_ids_seen: - report_info = await _get_report_for_snapshot( - session, report_id - ) + report_info = await _get_report_for_snapshot(session, report_id) if report_info: reports_data.append(report_info) report_ids_seen.add(report_id) @@ -816,9 +814,7 @@ async def get_snapshot_report_versions( return [] reports = snapshot.snapshot_data.get("reports", []) - siblings = [ - r for r in reports if r.get("report_group_id") == report_group_id - ] + siblings = [r for r in reports if r.get("report_group_id") == report_group_id] # Sort by original_id (ascending = insertion order ≈ created_at order) siblings.sort(key=lambda r: r.get("original_id", 0)) diff --git a/surfsense_web/atoms/chat/report-panel.atom.ts b/surfsense_web/atoms/chat/report-panel.atom.ts index 61860912c..0abb0c0ff 100644 --- a/surfsense_web/atoms/chat/report-panel.atom.ts +++ b/surfsense_web/atoms/chat/report-panel.atom.ts @@ -50,4 +50,3 @@ export const openReportPanelAtom = atom( export const closeReportPanelAtom = atom(null, (_, set) => { set(reportPanelAtom, initialState); }); - diff --git a/surfsense_web/components/markdown-viewer.tsx b/surfsense_web/components/markdown-viewer.tsx index 1b90cf902..192b0f0ba 100644 --- a/surfsense_web/components/markdown-viewer.tsx +++ b/surfsense_web/components/markdown-viewer.tsx @@ -6,7 +6,7 @@ import "katex/dist/katex.min.css"; import { cn } from "@/lib/utils"; const code = createCodePlugin({ - themes: ["nord", "nord"] + themes: ["nord", "nord"], }); const math = createMathPlugin({ @@ -24,9 +24,7 @@ interface MarkdownViewerProps { */ function stripOuterMarkdownFence(content: string): string { const trimmed = content.trim(); - const match = trimmed.match( - /^```(?:markdown|md)?\s*\n([\s\S]+?)\n```\s*$/ - ); + const match = trimmed.match(/^```(?:markdown|md)?\s*\n([\s\S]+?)\n```\s*$/); return match ? match[1] : content; } @@ -121,8 +119,18 @@ export function MarkdownViewer({ content, className }: MarkdownViewerProps) {
+ {description} +
)} {/* Metadata row */} @@ -305,7 +307,6 @@ export function Article({ )}- {error} -
+{error}
@@ -148,10 +143,7 @@ function ReportCard({ if (parsed.success) { // Check if report was marked as failed in metadata if (parsed.data.report_metadata?.status === "failed") { - setError( - parsed.data.report_metadata?.error_message || - "Report generation failed" - ); + setError(parsed.data.report_metadata?.error_message || "Report generation failed"); } else { // Determine version label from versions array let versionLabel: string | null = null; @@ -164,8 +156,7 @@ function ReportCard({ } setMetadata({ title: parsed.data.title || title, - wordCount: - parsed.data.report_metadata?.word_count ?? wordCount ?? null, + wordCount: parsed.data.report_metadata?.word_count ?? wordCount ?? null, versionLabel, }); } @@ -219,9 +210,10 @@ function ReportCard({ ) : ( <> - {metadata.wordCount != null && - `${metadata.wordCount.toLocaleString()} words`} - {metadata.wordCount != null && metadata.versionLabel &&