Skip to content

Commit bc5ddd1

Browse files
committed
Refactor code structure for improved readability and maintainability
1 parent 17120cf commit bc5ddd1

File tree

15 files changed

+1143
-238
lines changed

15 files changed

+1143
-238
lines changed

dashboard/__init__.py

Whitespace-only changes.

dashboard/core/data_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def load_json_report(file_path: Union[str, Path]) -> Dict[str, Any]:
2828
raise FileNotFoundError(f"Report file not found: {path}")
2929

3030
try:
31-
with open(path, 'r', encoding='utf-8') as f:
31+
with open(path, 'r',) as f:
3232
return json.load(f)
3333
except json.JSONDecodeError as e:
3434
raise ValueError(f"Invalid JSON format in {path}: {e}")

dashboard/datastar_app/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# VeritaScribe Datastar Dashboard
2+
3+
This directory contains the `datastar`-powered rewrite of the VeritaScribe error management dashboard.
4+
5+
## Overview
6+
7+
This application provides a real-time, interactive interface for analyzing and managing VeritaScribe error reports. It is built with modern, Python-centric tools to ensure a clean, maintainable, and highly reactive user experience without the need for custom JavaScript.
8+
9+
## Technology Stack
10+
11+
- **Backend Framework:** [`fasthtml`](https://github.com/AnswerDotAI/fasthtml)
12+
- **Reactivity:** [`datastar`](https://github.com/starfederation/datastar) and [`datastar-py`](https://github.com/starfederation/datastar-py)
13+
- **UI Components:** [`daisyUI`](https://daisyui.com/)
14+
- **Styling:** [`Tailwind CSS`](https://tailwindcss.com/) (via CDN)
15+
16+
## Project Structure
17+
18+
The application is organized into the following key files:
19+
20+
- [`app.py`](dashboard/datastar_app/app.py): The main `fasthtml` application. It defines all URL routes, handles server-side logic, and streams updates to the client using Server-Sent Events (SSE).
21+
- [`stores.py`](dashboard/datastar_app/stores.py): Defines the server-side state management using a simple Python dataclass. The `app_state` object holds the application's state, which is projected to the UI via `datastar` signals.
22+
- [`components.py`](dashboard/datastar_app/components.py): Contains reusable functions that generate `daisyui`-styled HTML components. These functions are used by the routes in `app.py` to build the UI.
23+
- `static/`: This directory is reserved for any static assets, although this project currently relies on CDNs for all frontend dependencies.
24+
25+
## Running the Application
26+
27+
To run the dashboard, execute the main launch script from the root of the project:
28+
29+
```bash
30+
python dashboard/launch_fasthtml.py
31+
```
32+
33+
The application will be available at `http://localhost:8000`.

dashboard/datastar_app/__init__.py

Whitespace-only changes.

dashboard/datastar_app/app.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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

Comments
 (0)