fxsab commited on
Commit
59d56ca
·
1 Parent(s): de02ad8

Prepare app for local and Docker deploy

Browse files
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ .git
2
+ .venv
3
+ __pycache__
4
+ uploads
5
+ data/analysis_history.json
6
+ archive (3)
7
+ synthetic_prescription_dataset
.env.example ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ APP_NAME=SimpliScribe
2
+ APP_ENV=development
3
+ HUGGINGFACEHUB_API_TOKEN=
4
+ HF_CHAT_MODEL=Qwen/Qwen2.5-7B-Instruct
5
+ INFERENCE_PROVIDER=fallback
6
+ MODEL_API_URL=
7
+ MODEL_API_KEY=
8
+ MAX_UPLOAD_MB=10
9
+ REQUEST_TIMEOUT_SECONDS=60
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .venv/
6
+ archive (3)/
7
+ synthetic_prescription_dataset/
8
+ uploads/*
9
+ data/analysis_history.json
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1
4
+ ENV PYTHONUNBUFFERED=1
5
+
6
+ WORKDIR /app
7
+
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ COPY . .
12
+
13
+ EXPOSE 7860
14
+
15
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SimpliScribe
2
+
3
+ SimpliScribe is a FastAPI application that extracts text from prescription images or PDFs and turns that OCR output into a structured medication summary.
4
+
5
+ ## Runtime options
6
+
7
+ - `INFERENCE_PROVIDER=fallback`
8
+ Uses a local heuristic fallback and does not require external model credentials.
9
+ - `INFERENCE_PROVIDER=huggingface`
10
+ Uses the Hugging Face Inference API with `HUGGINGFACEHUB_API_TOKEN` and `HF_CHAT_MODEL`.
11
+ - `INFERENCE_PROVIDER=endpoint`
12
+ Sends OCR text to a compatible HTTP endpoint using `MODEL_API_URL` and optional `MODEL_API_KEY`.
13
+
14
+ ## Local development
15
+
16
+ ```bash
17
+ python -m venv .venv
18
+ .venv\Scripts\activate
19
+ pip install -r requirements.txt
20
+ copy .env.example .env
21
+ uvicorn app:app --reload
22
+ ```
23
+
24
+ Open `http://127.0.0.1:8000`.
25
+
26
+ ## Testing
27
+
28
+ ```bash
29
+ pytest
30
+ ```
31
+
32
+ ## Docker deployment
33
+
34
+ ```bash
35
+ docker build -t simpliscribe .
36
+ docker run -p 7860:7860 --env-file .env simpliscribe
37
+ ```
38
+
39
+ ## Hugging Face Spaces
40
+
41
+ This project is ready for a Docker Space.
42
+
43
+ 1. Create a new Hugging Face Space with the Docker SDK.
44
+ 2. Push this repository to the Space.
45
+ 3. Add the environment variables from `.env.example` in the Space settings.
46
+ 4. Expose port `7860`.
47
+
48
+ For an external hosted model, set `INFERENCE_PROVIDER=endpoint` and point `MODEL_API_URL` at your inference endpoint.
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ easyocr
2
+ fastapi
3
+ huggingface_hub
4
+ httpx
5
+ jinja2
6
+ Pillow
7
+ PyMuPDF
8
+ pytest
9
+ python-dotenv
10
+ python-multipart
11
+ uvicorn[standard]
simpliscribe/web.py CHANGED
@@ -47,9 +47,9 @@ async def save_upload(file: UploadFile) -> Path:
47
 
48
  async def render_dashboard(request: Request, templates) -> HTMLResponse:
49
  return templates.TemplateResponse(
 
50
  "dashboard.html",
51
  {
52
- "request": request,
53
  "recent_analyses": load_history()[:5],
54
  "max_upload_mb": settings.max_upload_mb,
55
  "app_name": settings.app_name,
@@ -58,20 +58,14 @@ async def render_dashboard(request: Request, templates) -> HTMLResponse:
58
 
59
 
60
  async def render_history(request: Request, templates) -> HTMLResponse:
61
- return templates.TemplateResponse(
62
- "history.html",
63
- {"request": request, "analyses": load_history(), "app_name": settings.app_name},
64
- )
65
 
66
 
67
  async def render_details(request: Request, analysis_id: str, templates) -> HTMLResponse:
68
  analysis = get_analysis_record(analysis_id)
69
  if analysis is None:
70
  raise HTTPException(status_code=404, detail="Analysis not found.")
71
- return templates.TemplateResponse(
72
- "details.html",
73
- {"request": request, "analysis": analysis, "app_name": settings.app_name},
74
- )
75
 
76
 
77
  async def history_payload() -> dict[str, Any]:
 
47
 
48
  async def render_dashboard(request: Request, templates) -> HTMLResponse:
49
  return templates.TemplateResponse(
50
+ request,
51
  "dashboard.html",
52
  {
 
53
  "recent_analyses": load_history()[:5],
54
  "max_upload_mb": settings.max_upload_mb,
55
  "app_name": settings.app_name,
 
58
 
59
 
60
  async def render_history(request: Request, templates) -> HTMLResponse:
61
+ return templates.TemplateResponse(request, "history.html", {"analyses": load_history(), "app_name": settings.app_name})
 
 
 
62
 
63
 
64
  async def render_details(request: Request, analysis_id: str, templates) -> HTMLResponse:
65
  analysis = get_analysis_record(analysis_id)
66
  if analysis is None:
67
  raise HTTPException(status_code=404, detail="Analysis not found.")
68
+ return templates.TemplateResponse(request, "details.html", {"analysis": analysis, "app_name": settings.app_name})
 
 
 
69
 
70
 
71
  async def history_payload() -> dict[str, Any]:
templates/dashboard.html ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
6
+ <title>{{ app_name }} | Prescription Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"/>
9
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
11
+ <script id="tailwind-config">
12
+ tailwind.config = {
13
+ darkMode: "class",
14
+ theme: {
15
+ extend: {
16
+ colors: {
17
+ "primary": "#6600ff",
18
+ "background-light": "#f7f5f8",
19
+ "background-dark": "#170f23",
20
+ },
21
+ fontFamily: {
22
+ "display": ["Inter"]
23
+ },
24
+ borderRadius: {"DEFAULT": "0.5rem", "lg": "1rem", "xl": "1.5rem", "full": "9999px"},
25
+ },
26
+ },
27
+ }
28
+ </script>
29
+ <style>
30
+ body { font-family: 'Inter', sans-serif; }
31
+ </style>
32
+ </head>
33
+ <body class="bg-background-light dark:bg-background-dark text-slate-900 dark:text-slate-100 min-h-screen">
34
+ <header class="sticky top-0 z-50 border-b border-primary/10 bg-background-light/80 dark:bg-background-dark/80 backdrop-blur-md">
35
+ <div class="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
36
+ <div class="flex items-center gap-3">
37
+ <div class="w-10 h-10 bg-primary rounded-lg flex items-center justify-center text-white">
38
+ <span class="material-symbols-outlined text-2xl">health_metrics</span>
39
+ </div>
40
+ <h1 class="text-xl font-bold tracking-tight text-slate-900 dark:text-white">{{ app_name }}</h1>
41
+ </div>
42
+ <nav class="hidden md:flex items-center gap-10">
43
+ <a class="text-sm font-semibold text-primary" href="/">Dashboard</a>
44
+ <a class="text-sm font-medium text-slate-600 dark:text-slate-400 hover:text-primary transition-colors" href="/history">History</a>
45
+ <a class="text-sm font-medium text-slate-600 dark:text-slate-400 hover:text-primary transition-colors" href="#recent-activity">Recent Activity</a>
46
+ </nav>
47
+ <span class="rounded-full border border-primary/20 bg-primary/5 px-4 py-2 text-xs font-semibold text-primary">Configurable inference pipeline</span>
48
+ </div>
49
+ </header>
50
+ <main class="max-w-5xl mx-auto px-6 py-12">
51
+ <section class="text-center mb-16">
52
+ <h2 class="text-4xl md:text-5xl font-extrabold text-slate-900 dark:text-white mb-4 tracking-tight">
53
+ Your Health, <span class="text-primary">Decoded.</span>
54
+ </h2>
55
+ <p class="text-lg text-slate-600 dark:text-slate-400 max-w-2xl mx-auto leading-relaxed">
56
+ Upload a prescription image or PDF and convert OCR output into a cleaner medication summary that is easier to review.
57
+ </p>
58
+ </section>
59
+
60
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-16">
61
+ <div class="bg-white dark:bg-slate-900/50 p-8 rounded-xl border border-primary/10 shadow-sm flex flex-col items-center justify-center text-center">
62
+ <form id="upload-form" class="w-full h-64 border-2 border-dashed border-primary/30 rounded-xl bg-primary/5 flex flex-col items-center justify-center group cursor-pointer hover:border-primary hover:bg-primary/10 transition-all relative">
63
+ <input type="file" id="prescription_image" name="file" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" accept="image/png, image/jpeg, image/webp, application/pdf">
64
+ <span class="material-symbols-outlined text-4xl text-primary mb-3">cloud_upload</span>
65
+ <p class="text-slate-900 dark:text-white font-semibold mb-1" id="upload-text">Drag and drop prescription</p>
66
+ <p class="text-xs text-slate-500 mb-6 uppercase tracking-widest font-bold">PDF, JPG, PNG, or WEBP</p>
67
+ <button type="button" class="bg-primary text-white px-6 py-3 rounded-lg font-bold text-sm shadow-lg shadow-primary/20 hover:scale-[1.02] transition-transform pointer-events-none">
68
+ Select File
69
+ </button>
70
+ </form>
71
+ <div class="mt-4 w-full text-center">
72
+ <button id="analyze-btn" class="hidden bg-slate-900 text-white dark:bg-white dark:text-slate-900 px-8 py-3 rounded-lg font-bold text-sm shadow-lg hover:scale-[1.02] transition-transform mx-auto">Analyze Now</button>
73
+ </div>
74
+ <p class="mt-4 text-xs text-slate-500">Maximum upload size: {{ max_upload_mb }} MB</p>
75
+ <div class="mt-6 flex items-center gap-2 text-slate-500 text-xs text-center mx-auto justify-center">
76
+ <span class="material-symbols-outlined text-sm">info</span>
77
+ Results are stored locally in the workspace history file until you clear them.
78
+ </div>
79
+ </div>
80
+
81
+ <div class="relative group">
82
+ <div class="absolute -inset-1 bg-gradient-to-r from-primary/20 to-primary/5 rounded-2xl blur opacity-25"></div>
83
+ <div class="relative h-full bg-white dark:bg-slate-800 rounded-xl border border-primary/10 overflow-hidden shadow-xl">
84
+ <div class="absolute top-0 left-0 w-full h-1 bg-primary"></div>
85
+ <div class="p-6 h-full flex flex-col">
86
+ <div class="flex items-center justify-between mb-4">
87
+ <h3 class="text-sm font-bold uppercase tracking-wider text-slate-400">Scan Preview</h3>
88
+ <span id="scan-status" class="px-2 py-1 bg-slate-100 text-slate-700 dark:bg-slate-900/30 dark:text-slate-400 text-[10px] font-bold rounded uppercase">Awaiting File</span>
89
+ </div>
90
+ <div class="flex-1 rounded-lg bg-slate-50 dark:bg-slate-900/80 border border-slate-100 dark:border-slate-700 overflow-hidden relative flex items-center justify-center">
91
+ <div class="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-5"></div>
92
+ <img id="preview-image" class="hidden w-full h-full object-cover" src="" alt="Preview"/>
93
+ <div id="pdf-preview-label" class="hidden absolute inset-6 rounded-2xl border border-dashed border-primary/30 bg-white/80 dark:bg-slate-800/80 px-6 py-8 text-center">
94
+ <span class="material-symbols-outlined text-5xl text-primary">picture_as_pdf</span>
95
+ <p class="mt-4 text-sm font-bold text-slate-900 dark:text-white">PDF document ready</p>
96
+ <p class="mt-2 text-xs text-slate-500">The first page will be converted before OCR.</p>
97
+ </div>
98
+ <div id="processing-overlay" class="hidden absolute inset-0 flex items-center justify-center bg-slate-900/10 dark:bg-slate-100/10 backdrop-blur-sm z-10 transition-opacity">
99
+ <div class="bg-white/90 dark:bg-slate-800/90 px-4 py-2 rounded-full shadow-lg border border-primary/20 flex items-center gap-2">
100
+ <span class="w-2 h-2 bg-primary rounded-full animate-pulse"></span>
101
+ <span class="text-xs font-bold text-slate-700 dark:text-slate-200">Processing Document...</span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <section id="results-area" class="hidden">
111
+ <div class="flex items-center justify-between mb-8">
112
+ <h3 class="text-2xl font-bold text-slate-900 dark:text-white">Analyzed Medications</h3>
113
+ <button id="download-report-btn" class="text-primary text-sm font-bold flex items-center gap-1 hover:underline">
114
+ Download Report
115
+ <span class="material-symbols-outlined text-sm">download</span>
116
+ </button>
117
+ </div>
118
+
119
+ <div id="medications-container" class="space-y-6"></div>
120
+ </section>
121
+
122
+ <section id="recent-activity" class="mt-16">
123
+ <div class="flex items-center justify-between gap-4 mb-6">
124
+ <div>
125
+ <h3 class="text-2xl font-bold text-slate-900 dark:text-white">Recent Activity</h3>
126
+ <p class="text-sm text-slate-500">The latest saved analyses from this workspace.</p>
127
+ </div>
128
+ <a href="/history" class="text-sm font-bold text-primary hover:underline">View all history</a>
129
+ </div>
130
+ {% if recent_analyses %}
131
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
132
+ {% for analysis in recent_analyses %}
133
+ <a href="/details/{{ analysis.id }}" class="block rounded-xl border border-primary/10 bg-white dark:bg-slate-900/50 p-5 shadow-sm hover:shadow-md transition-shadow">
134
+ <p class="text-[11px] uppercase tracking-[0.2em] text-slate-500 font-bold">{{ analysis.created_at }}</p>
135
+ <h4 class="mt-2 text-lg font-bold text-slate-900 dark:text-white">{{ analysis.filename }}</h4>
136
+ <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
137
+ {{ analysis.medications|length }} medication{% if analysis.medications|length != 1 %}s{% endif %} extracted
138
+ </p>
139
+ </a>
140
+ {% endfor %}
141
+ </div>
142
+ {% else %}
143
+ <div class="rounded-xl border border-dashed border-primary/20 bg-white/70 dark:bg-slate-900/40 p-8 text-center text-sm text-slate-500">
144
+ No saved analyses yet. Your recent scans will appear here.
145
+ </div>
146
+ {% endif %}
147
+ </section>
148
+ </main>
149
+
150
+ <template id="medication-card-template">
151
+ <div class="bg-white dark:bg-slate-900 rounded-xl border border-primary/10 overflow-hidden shadow-sm hover:shadow-md transition-shadow">
152
+ <div class="flex flex-col gap-6 p-8">
153
+ <div class="flex flex-col md:flex-row md:items-start justify-between gap-4">
154
+ <div class="flex items-start gap-4">
155
+ <div class="med-badge flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-primary/10 text-lg font-black text-primary"></div>
156
+ <div>
157
+ <div class="flex items-center gap-3 flex-wrap">
158
+ <h4 class="med-name text-2xl font-bold text-slate-900 dark:text-white"></h4>
159
+ <span class="med-type bg-primary text-white text-[10px] font-black px-2 py-1 rounded-full uppercase tracking-widest"></span>
160
+ </div>
161
+ <p class="med-desc mt-2 text-slate-500 text-sm"></p>
162
+ </div>
163
+ </div>
164
+ <a href="#" class="med-details-link bg-slate-50 dark:bg-slate-800 hover:bg-primary/10 hover:text-primary transition-all text-slate-600 dark:text-slate-300 px-4 py-2 rounded-lg text-sm font-bold border border-slate-100 dark:border-slate-700">
165
+ Full Details
166
+ </a>
167
+ </div>
168
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
169
+ <div class="p-3 rounded-lg bg-background-light dark:bg-slate-800/50">
170
+ <p class="text-[10px] uppercase font-bold text-slate-400 mb-1">Dosage</p>
171
+ <p class="med-dosage text-sm font-bold text-slate-800 dark:text-slate-200"></p>
172
+ </div>
173
+ <div class="p-3 rounded-lg bg-background-light dark:bg-slate-800/50">
174
+ <p class="text-[10px] uppercase font-bold text-slate-400 mb-1">Frequency</p>
175
+ <p class="med-frequency text-sm font-bold text-slate-800 dark:text-slate-200"></p>
176
+ </div>
177
+ <div class="p-3 rounded-lg bg-background-light dark:bg-slate-800/50">
178
+ <p class="text-[10px] uppercase font-bold text-slate-400 mb-1">Duration</p>
179
+ <p class="med-duration text-sm font-bold text-slate-800 dark:text-slate-200"></p>
180
+ </div>
181
+ </div>
182
+ <div class="p-4 rounded-lg bg-primary/5 border border-primary/10 flex gap-3">
183
+ <span class="material-symbols-outlined text-primary text-xl">fact_check</span>
184
+ <div>
185
+ <p class="text-xs font-bold text-primary uppercase tracking-tighter mb-1">Medication Note</p>
186
+ <p class="med-insight text-sm text-slate-700 dark:text-slate-300 leading-relaxed"></p>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ </template>
192
+
193
+ <footer class="mt-20 border-t border-primary/10 py-12 bg-white dark:bg-slate-950">
194
+ <div class="max-w-7xl mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-8">
195
+ <div class="flex items-center gap-3 grayscale opacity-70">
196
+ <div class="w-8 h-8 bg-slate-400 rounded-lg flex items-center justify-center text-white">
197
+ <span class="material-symbols-outlined text-lg">health_metrics</span>
198
+ </div>
199
+ <h1 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white">{{ app_name }}</h1>
200
+ </div>
201
+ <div class="flex items-center gap-2 bg-slate-100 dark:bg-slate-900/30 px-4 py-2 rounded-full border border-slate-200 dark:border-slate-700">
202
+ <span class="material-symbols-outlined text-slate-600 dark:text-slate-300 text-lg">memory</span>
203
+ <span class="text-xs font-bold text-slate-700 dark:text-slate-300 tracking-wide uppercase">Local history enabled</span>
204
+ </div>
205
+ </div>
206
+ </footer>
207
+
208
+ <script>
209
+ const fileInput = document.getElementById('prescription_image');
210
+ const previewImage = document.getElementById('preview-image');
211
+ const uploadText = document.getElementById('upload-text');
212
+ const scanStatus = document.getElementById('scan-status');
213
+ const analyzeBtn = document.getElementById('analyze-btn');
214
+ const pdfPreviewLabel = document.getElementById('pdf-preview-label');
215
+ const processingOverlay = document.getElementById('processing-overlay');
216
+ const resultsArea = document.getElementById('results-area');
217
+ const medicationsContainer = document.getElementById('medications-container');
218
+ const template = document.getElementById('medication-card-template');
219
+
220
+ const pdfPlaceholderSvg = `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(`
221
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800">
222
+ <rect width="1200" height="800" fill="#f8fafc"/>
223
+ <rect x="320" y="120" width="560" height="560" rx="42" fill="#ffffff" stroke="#e2e8f0" stroke-width="12"/>
224
+ <text x="600" y="330" text-anchor="middle" fill="#6600ff" font-size="80" font-family="Arial, sans-serif">PDF</text>
225
+ <text x="600" y="420" text-anchor="middle" fill="#475569" font-size="32" font-family="Arial, sans-serif">Preview generated from first page at scan time</text>
226
+ </svg>
227
+ `)}`;
228
+
229
+ fileInput.addEventListener('change', function(e) {
230
+ if(e.target.files && e.target.files[0]) {
231
+ const file = e.target.files[0];
232
+ uploadText.innerText = file.name;
233
+ scanStatus.className = "px-2 py-1 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 text-[10px] font-bold rounded uppercase";
234
+ scanStatus.innerText = "Ready to Scan";
235
+ analyzeBtn.classList.remove('hidden');
236
+
237
+ if (file.type === 'application/pdf') {
238
+ previewImage.src = pdfPlaceholderSvg;
239
+ previewImage.classList.remove('hidden');
240
+ pdfPreviewLabel.classList.remove('hidden');
241
+ return;
242
+ }
243
+
244
+ pdfPreviewLabel.classList.add('hidden');
245
+ const reader = new FileReader();
246
+ reader.onload = function(loadEvent) {
247
+ previewImage.src = loadEvent.target.result;
248
+ previewImage.classList.remove('hidden');
249
+ };
250
+ reader.readAsDataURL(file);
251
+ }
252
+ });
253
+
254
+ analyzeBtn.addEventListener('click', async function() {
255
+ if(!fileInput.files[0]) return;
256
+
257
+ processingOverlay.classList.remove('hidden');
258
+ scanStatus.innerText = "Active Scan";
259
+ analyzeBtn.disabled = true;
260
+ analyzeBtn.innerHTML = "Processing...";
261
+ analyzeBtn.classList.add("opacity-50");
262
+
263
+ const formData = new FormData();
264
+ formData.append("file", fileInput.files[0]);
265
+
266
+ try {
267
+ const response = await fetch('/api/analyze', {
268
+ method: 'POST',
269
+ body: formData
270
+ });
271
+
272
+ const data = await response.json();
273
+ if(!response.ok) throw new Error(data.error || data.detail || "Server Error");
274
+
275
+ medicationsContainer.innerHTML = '';
276
+
277
+ if (data.medications && data.medications.length > 0) {
278
+ data.medications.forEach(med => {
279
+ const clone = template.content.cloneNode(true);
280
+
281
+ clone.querySelector('.med-name').textContent = med.name || 'Unknown';
282
+ clone.querySelector('.med-desc').textContent = med.category || 'General Medication';
283
+ clone.querySelector('.med-dosage').textContent = med.dosage || 'N/A';
284
+ clone.querySelector('.med-frequency').textContent = med.frequency || 'N/A';
285
+ clone.querySelector('.med-duration').textContent = med.duration || 'N/A';
286
+ clone.querySelector('.med-insight').textContent = med.insight || 'Follow doctor\'s instructions carefully.';
287
+ clone.querySelector('.med-type').textContent = med.type || 'MEDICATION';
288
+ const badgeText = (med.name || 'M').slice(0, 2).toUpperCase();
289
+ clone.querySelector('.med-badge').textContent = badgeText;
290
+ clone.querySelector('.med-details-link').href = data.analysis_id ? `/details/${data.analysis_id}` : '/history';
291
+
292
+ medicationsContainer.appendChild(clone);
293
+ });
294
+
295
+ scanStatus.className = "px-2 py-1 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 text-[10px] font-bold rounded uppercase";
296
+ scanStatus.innerText = "Scan Complete";
297
+ resultsArea.classList.remove('hidden');
298
+ resultsArea.scrollIntoView({ behavior: 'smooth' });
299
+ } else {
300
+ alert(data.error || "Could not extract any medications. Please try another image.");
301
+ scanStatus.className = "px-2 py-1 bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 text-[10px] font-bold rounded uppercase";
302
+ scanStatus.innerText = "Scan Failed";
303
+ }
304
+
305
+ } catch(err) {
306
+ console.error(err);
307
+ alert(err.message || "Error analyzing prescription. Make sure backend is running.");
308
+ scanStatus.className = "px-2 py-1 bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 text-[10px] font-bold rounded uppercase";
309
+ scanStatus.innerText = "Error";
310
+ } finally {
311
+ processingOverlay.classList.add('hidden');
312
+ analyzeBtn.disabled = false;
313
+ analyzeBtn.innerHTML = "Analyze Again";
314
+ analyzeBtn.classList.remove("opacity-50");
315
+ }
316
+ });
317
+
318
+ const downloadReportBtn = document.getElementById('download-report-btn');
319
+ if(downloadReportBtn) {
320
+ downloadReportBtn.addEventListener('click', function() {
321
+ const resultsAreaToPrint = document.getElementById('results-area');
322
+ const opt = {
323
+ margin: 10,
324
+ filename: 'SimpliScribe_Report.pdf',
325
+ image: { type: 'jpeg', quality: 0.98 },
326
+ html2canvas: { scale: 2 },
327
+ jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
328
+ };
329
+ html2pdf().set(opt).from(resultsAreaToPrint).save();
330
+ });
331
+ }
332
+ </script>
333
+ </body>
334
+ </html>
templates/details.html ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
6
+ <title>{{ app_name }} | Analysis Details</title>
7
+ <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"/>
9
+ <style>
10
+ body { font-family: 'Inter', sans-serif; }
11
+ </style>
12
+ </head>
13
+ <body class="min-h-screen bg-slate-50 text-slate-900">
14
+ <header class="border-b border-slate-200 bg-white/90 backdrop-blur sticky top-0 z-20">
15
+ <div class="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
16
+ <a href="/" class="text-xl font-bold tracking-tight text-slate-900">{{ app_name }}</a>
17
+ <nav class="flex items-center gap-6 text-sm font-semibold">
18
+ <a href="/" class="text-slate-500 hover:text-slate-900">Dashboard</a>
19
+ <a href="/history" class="text-slate-500 hover:text-slate-900">History</a>
20
+ </nav>
21
+ </div>
22
+ </header>
23
+ <main class="max-w-6xl mx-auto px-6 py-10">
24
+ <div class="mb-8 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
25
+ <div>
26
+ <p class="text-xs font-bold uppercase tracking-[0.2em] text-slate-500">Analysis Detail</p>
27
+ <h1 class="mt-1 text-3xl font-extrabold tracking-tight">{{ analysis.filename }}</h1>
28
+ <p class="mt-2 text-sm text-slate-600">Created at {{ analysis.created_at }}</p>
29
+ </div>
30
+ <a href="/history" class="inline-flex items-center justify-center rounded-lg border border-slate-300 px-4 py-2 text-sm font-bold text-slate-800 hover:border-slate-900 hover:text-slate-900">
31
+ Back to History
32
+ </a>
33
+ </div>
34
+
35
+ <section class="grid gap-8 lg:grid-cols-[1.4fr_0.9fr]">
36
+ <div class="space-y-4">
37
+ {% for med in analysis.medications %}
38
+ <article class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
39
+ <div class="grid gap-0 md:grid-cols-[160px_1fr]">
40
+ <div class="flex min-h-[220px] items-center justify-center bg-primary/5 text-4xl font-black text-primary">
41
+ {{ med.name[:2]|upper }}
42
+ </div>
43
+ <div class="p-6">
44
+ <div class="flex flex-wrap items-start justify-between gap-3">
45
+ <div>
46
+ <h2 class="text-2xl font-bold text-slate-900">{{ med.name }}</h2>
47
+ <p class="mt-1 text-sm text-slate-500">{{ med.category }}</p>
48
+ </div>
49
+ <span class="rounded-full bg-slate-900 px-3 py-1 text-xs font-bold uppercase tracking-[0.2em] text-white">{{ med.type }}</span>
50
+ </div>
51
+ <div class="mt-6 grid gap-3 sm:grid-cols-3">
52
+ <div class="rounded-xl bg-slate-50 p-4">
53
+ <p class="text-xs font-bold uppercase tracking-[0.15em] text-slate-500">Dosage</p>
54
+ <p class="mt-2 text-sm font-bold text-slate-900">{{ med.dosage }}</p>
55
+ </div>
56
+ <div class="rounded-xl bg-slate-50 p-4">
57
+ <p class="text-xs font-bold uppercase tracking-[0.15em] text-slate-500">Frequency</p>
58
+ <p class="mt-2 text-sm font-bold text-slate-900">{{ med.frequency }}</p>
59
+ </div>
60
+ <div class="rounded-xl bg-slate-50 p-4">
61
+ <p class="text-xs font-bold uppercase tracking-[0.15em] text-slate-500">Duration</p>
62
+ <p class="mt-2 text-sm font-bold text-slate-900">{{ med.duration }}</p>
63
+ </div>
64
+ </div>
65
+ <div class="mt-6 rounded-xl border border-emerald-100 bg-emerald-50 p-4 text-sm text-emerald-900">
66
+ {{ med.insight }}
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </article>
71
+ {% endfor %}
72
+ </div>
73
+
74
+ <aside class="space-y-6">
75
+ <section class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
76
+ <h2 class="text-lg font-bold text-slate-900">Raw OCR Text</h2>
77
+ <p class="mt-4 whitespace-pre-wrap break-words text-sm leading-7 text-slate-700">{{ analysis.raw_text }}</p>
78
+ </section>
79
+ <section class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
80
+ <h2 class="text-lg font-bold text-slate-900">Summary</h2>
81
+ <p class="mt-4 text-sm text-slate-700">
82
+ {{ analysis.medications|length }} medication{% if analysis.medications|length != 1 %}s{% endif %} were identified from this document.
83
+ </p>
84
+ </section>
85
+ </aside>
86
+ </section>
87
+ </main>
88
+ </body>
89
+ </html>
templates/history.html ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
6
+ <title>{{ app_name }} | Analysis History</title>
7
+ <script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"/>
9
+ <style>
10
+ body { font-family: 'Inter', sans-serif; }
11
+ </style>
12
+ </head>
13
+ <body class="min-h-screen bg-slate-50 text-slate-900">
14
+ <header class="border-b border-slate-200 bg-white/90 backdrop-blur sticky top-0 z-20">
15
+ <div class="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
16
+ <a href="/" class="text-xl font-bold tracking-tight text-slate-900">{{ app_name }}</a>
17
+ <nav class="flex items-center gap-6 text-sm font-semibold">
18
+ <a href="/" class="text-slate-500 hover:text-slate-900">Dashboard</a>
19
+ <a href="/history" class="text-primary-700 text-slate-900">History</a>
20
+ </nav>
21
+ </div>
22
+ </header>
23
+ <main class="max-w-6xl mx-auto px-6 py-10">
24
+ <div class="flex items-end justify-between gap-4 mb-8">
25
+ <div>
26
+ <p class="text-sm uppercase tracking-[0.2em] text-slate-500 font-semibold">Saved Analyses</p>
27
+ <h1 class="text-3xl font-extrabold tracking-tight">Prescription analysis history</h1>
28
+ </div>
29
+ <a href="/" class="inline-flex items-center rounded-lg bg-slate-900 px-4 py-2 text-sm font-bold text-white">New Analysis</a>
30
+ </div>
31
+
32
+ {% if analyses %}
33
+ <div class="grid gap-4">
34
+ {% for analysis in analyses %}
35
+ <article class="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
36
+ <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
37
+ <div>
38
+ <p class="text-xs font-bold uppercase tracking-[0.2em] text-slate-500">{{ analysis.created_at }}</p>
39
+ <h2 class="mt-1 text-xl font-bold text-slate-900">{{ analysis.filename }}</h2>
40
+ <p class="mt-2 text-sm text-slate-600">
41
+ {{ analysis.medications|length }} medication{% if analysis.medications|length != 1 %}s{% endif %} extracted
42
+ </p>
43
+ </div>
44
+ <a href="/details/{{ analysis.id }}" class="inline-flex items-center justify-center rounded-lg border border-slate-300 px-4 py-2 text-sm font-bold text-slate-800 hover:border-slate-900 hover:text-slate-900">
45
+ View Details
46
+ </a>
47
+ </div>
48
+ </article>
49
+ {% endfor %}
50
+ </div>
51
+ {% else %}
52
+ <section class="rounded-2xl border border-dashed border-slate-300 bg-white p-12 text-center">
53
+ <h2 class="text-2xl font-bold text-slate-900">No analyses saved yet</h2>
54
+ <p class="mt-3 text-sm text-slate-600">Run a prescription scan from the dashboard to start building your history.</p>
55
+ </section>
56
+ {% endif %}
57
+ </main>
58
+ </body>
59
+ </html>
tests/test_app.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi.testclient import TestClient
2
+
3
+ from simpliscribe.main import app
4
+
5
+
6
+ client = TestClient(app)
7
+
8
+
9
+ def test_dashboard_route():
10
+ response = client.get("/")
11
+ assert response.status_code == 200
12
+
13
+
14
+ def test_history_api_route():
15
+ response = client.get("/api/history")
16
+ assert response.status_code == 200
17
+ assert "analyses" in response.json()