1+ from fasthtml .common import *
2+ from typing import AsyncIterator , Optional , Dict , Any
3+ from starlette .responses import StreamingResponse
4+ import json
5+ import tempfile
6+ import os
7+ from core .error_manager import ErrorManager
8+ from core .data_processor import DataProcessor
9+ from core .models import FilterCriteria , ErrorStatus , ErrorType , Severity
10+ from .stores import app_state , project_signals
11+
12+
13+ # # Try to import datastar-py SSE helper; provide a minimal fallback if missing
14+ # try:
15+ from datastar_py import ServerSentEventGenerator as SSE # type: ignore
16+ # except Exception: # pragma: no cover
17+ # class SSE: # Fallback SSE generator
18+ # @staticmethod
19+ # def patch_elements(html: str) -> str:
20+ # # Minimal SSE format: the datastar client can interpret payloads as needed
21+ # return f"data: {json.dumps({'patch': {'elements': html}})}\n\n"
22+
23+ # @staticmethod
24+ # def patch_signals(signals: Dict[str, Any]) -> str:
25+ # return f"data: {json.dumps({'patch': {'signals': signals}})}\n\n"
26+
27+
28+ app , rt = fast_app ()
29+ error_manager = ErrorManager ()
30+
31+ # Load initial data on startup
32+ dp = DataProcessor ()
33+ initial_report = dp .load_and_convert_report ("demo_thesis.pdf" )
34+ error_manager .import_analysis_report (initial_report )
35+ app_state .selected_document = initial_report .document_path
36+ app_state .errors = error_manager .get_errors (app_state .selected_document )
37+
38+
39+ def sse_response (events : AsyncIterator [bytes ]) -> StreamingResponse :
40+ # Generic SSE response helper for FastHTML (Starlette ASGI)
41+ headers = {"Cache-Control" : "no-cache" , "X-Accel-Buffering" : "no" }
42+ return StreamingResponse (events , media_type = "text/event-stream" , headers = headers )
43+
44+
45+ async def stream_patch (elements_html : Optional [str ] = None , signals : Optional [Dict [str , Any ]] = None ) -> AsyncIterator [bytes ]:
46+ if elements_html :
47+ yield SSE .patch_elements (elements_html ).encode ()
48+ if signals is not None :
49+ yield SSE .patch_signals (signals ).encode ()
50+
51+
52+ @rt ("/" )
53+ def get_home ():
54+ # Initial shell; regions lazy-load via datastar actions on-load
55+ from .components import page_layout , filter_sidebar , stats_cards , error_table
56+ counts = {
57+ "total" : len (app_state .errors ),
58+ "pending" : sum (getattr (e , "status" , None ) == ErrorStatus .PENDING for e in app_state .errors ),
59+ "completed" : sum (getattr (e , "status" , None ) == ErrorStatus .RESOLVED for e in app_state .errors ),
60+ }
61+ return page_layout (
62+ "VeritaScribe Dashboard" ,
63+ filter_sidebar (),
64+ stats_cards (counts ),
65+ error_table (app_state .errors ),
66+ )
67+
68+
69+ @rt ("/dashboard/stats" )
70+ def get_stats ():
71+ from .components import stats_cards
72+ counts = {
73+ "total" : len (app_state .errors ),
74+ "pending" : sum (getattr (e , "status" , None ) == ErrorStatus .PENDING for e in app_state .errors ),
75+ "completed" : sum (getattr (e , "status" , None ) == ErrorStatus .RESOLVED for e in app_state .errors ),
76+ }
77+ html = str (stats_cards (counts )) # FastTags stringify to HTML
78+
79+ async def gen ():
80+ async for chunk in stream_patch (elements_html = html , signals = project_signals ()):
81+ yield chunk
82+
83+ return sse_response (gen ())
84+
85+
86+ @rt ("/dashboard/errors" )
87+ def get_errors ():
88+ from .components import error_table
89+ html = str (error_table (app_state .errors ))
90+
91+ async def gen ():
92+ async for chunk in stream_patch (elements_html = html ):
93+ yield chunk
94+
95+ return sse_response (gen ())
96+
97+
98+ @rt ("/dashboard/import_form" )
99+ def get_import_form ():
100+ from .components import upload_form
101+ html = str (upload_form ())
102+
103+ async def gen ():
104+ async for chunk in stream_patch (elements_html = html ):
105+ yield chunk
106+
107+ return sse_response (gen ())
108+
109+
110+ @rt ("/dashboard/filter" )
111+ def post_filter (status : str = "" , error_type : str = "" , severity : str = "" , q : str = "" ):
112+ # Build FilterCriteria and query errors via ErrorManager
113+ fc = FilterCriteria ()
114+
115+ # Normalize "all" sentinel values and map to enums if provided
116+ if status and status .lower () not in ("all" , "all statuses" ):
117+ try :
118+ fc .status = [ErrorStatus (status )]
119+ except Exception :
120+ pass
121+
122+ if error_type and error_type .lower () not in ("all" , "all types" ):
123+ try :
124+ fc .error_type = [ErrorType (error_type )]
125+ except Exception :
126+ pass
127+
128+ if severity and severity .lower () not in ("all" , "all severities" ):
129+ try :
130+ fc .severity = [Severity (severity )]
131+ except Exception :
132+ pass
133+
134+ if q and q .strip ():
135+ fc .search_text = q .strip ()
136+
137+ app_state .filter_criteria = fc
138+ app_state .errors = error_manager .get_errors (app_state .selected_document , fc )
139+
140+ from .components import error_table
141+ html = str (error_table (app_state .errors ))
142+
143+ async def gen ():
144+ async for chunk in stream_patch (elements_html = html , signals = project_signals ()):
145+ yield chunk
146+
147+ return sse_response (gen ())
148+
149+
150+ @rt ("/errors/{error_id}/status" )
151+ def post_error_status (error_id : str , status : str ):
152+ # Update in DB
153+ try :
154+ new_status = ErrorStatus (status )
155+ except Exception :
156+ new_status = None
157+
158+ if new_status is not None :
159+ error_manager .update_error (error_id , {"status" : new_status })
160+ # Update in-memory state to remain consistent
161+ for e in app_state .errors :
162+ if getattr (e , "error_id" , None ) == error_id :
163+ e .status = new_status
164+ break
165+
166+ from .components import error_row
167+ target = next ((e for e in app_state .errors if getattr (e , "error_id" , None ) == error_id ), None )
168+ row_html = str (error_row (target )) if target else ""
169+
170+ async def gen ():
171+ async for chunk in stream_patch (elements_html = row_html , signals = project_signals ()):
172+ yield chunk
173+
174+ return sse_response (gen ())
175+
176+
177+ @rt ("/errors/bulk" )
178+ def post_bulk (action : str ):
179+ # Apply action to selected errors from signals (if needed, read_signals could be parsed from request)
180+ updates : Dict [str , Any ] = {}
181+
182+ if action == "mark_resolved" :
183+ updates ["status" ] = ErrorStatus .RESOLVED
184+ elif action == "mark_dismissed" :
185+ updates ["status" ] = ErrorStatus .DISMISSED
186+ elif action == "assign_to_me" :
187+ updates ["assigned_to" ] = "web_user"
188+ updates ["status" ] = ErrorStatus .IN_PROGRESS
189+
190+ if updates and app_state .selected_errors :
191+ error_manager .bulk_update_errors (app_state .selected_errors , updates )
192+
193+ app_state .errors = error_manager .get_errors (app_state .selected_document , app_state .filter_criteria )
194+
195+ from .components import error_table
196+ html = str (error_table (app_state .errors ))
197+
198+ async def gen ():
199+ async for chunk in stream_patch (elements_html = html , signals = project_signals ()):
200+ yield chunk
201+
202+ return sse_response (gen ())
203+
204+
205+ @rt ("/upload" )
206+ async def post_upload (file : UploadFile ):
207+ dp = DataProcessor ()
208+ # Persist uploaded file temporarily for DataProcessor
209+ data = await file .read ()
210+ with tempfile .NamedTemporaryFile (mode = "wb" , suffix = ".json" , delete = False ) as tf :
211+ tf .write (data )
212+ temp_path = tf .name
213+
214+ try :
215+ report = dp .load_and_convert_report (temp_path )
216+ # Persist and reload via ErrorManager
217+ error_manager .import_analysis_report (report )
218+ app_state .selected_document = report .document_path
219+ app_state .errors = error_manager .get_errors (app_state .selected_document , app_state .filter_criteria )
220+
221+ from .components import stats_cards , error_table
222+ msg = Div ("Upload successful" , cls = "alert alert-success" )
223+ counts = {
224+ "total" : len (app_state .errors ),
225+ "pending" : sum (getattr (e , "status" , None ) == ErrorStatus .PENDING for e in app_state .errors ),
226+ "completed" : sum (getattr (e , "status" , None ) == ErrorStatus .RESOLVED for e in app_state .errors ),
227+ }
228+ html = str (Div (msg , stats_cards (counts ), error_table (app_state .errors )))
229+ finally :
230+ try :
231+ os .remove (temp_path )
232+ except Exception :
233+ pass
234+
235+ async def gen ():
236+ async for chunk in stream_patch (elements_html = html , signals = project_signals ()):
237+ yield chunk
238+
239+ return sse_response (gen ())
240+
241+
242+ if __name__ == "__main__" :
243+ serve ()
0 commit comments