evaldoc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.env.staging.example +19 -0
  2. package/.firebaserc +6 -0
  3. package/.github/workflows/auto-merge-to-main.yml +28 -0
  4. package/.github/workflows/ci.yml +27 -0
  5. package/.github/workflows/npxhub-publish.yml +170 -0
  6. package/.prettierignore +4 -0
  7. package/.prettierrc.json +7 -0
  8. package/README.md +142 -0
  9. package/SRS.md +448 -0
  10. package/apphosting.staging.yaml +48 -0
  11. package/apphosting.yaml +50 -0
  12. package/bin/evaldoc.cjs +105 -0
  13. package/cors.json +8 -0
  14. package/docs/ARCHITECTURE.md +769 -0
  15. package/docs/ARCHITECTURE_SUMMARY.md +323 -0
  16. package/docs/DEPLOYMENT.md +86 -0
  17. package/docs/DEPLOYMENT_STEPS.md +373 -0
  18. package/docs/DEPLOY_FIRESTORE_RULES.md +239 -0
  19. package/docs/DEPLOY_STORAGE_RULES.md +82 -0
  20. package/docs/DOCTOR_TUTORIAL.md +467 -0
  21. package/docs/FIREBASE_STORAGE_CORS_FIX.md +131 -0
  22. package/docs/FIXES_APPLIED.md +128 -0
  23. package/docs/IMPLEMENTATION.md +498 -0
  24. package/docs/IMPLEMENTATION_COMPLETE.md +317 -0
  25. package/docs/LLM-SETUP.md +216 -0
  26. package/docs/MANUAL_TEST_GUIDE.md +327 -0
  27. package/docs/PATIENT_GUIDE.md +451 -0
  28. package/docs/PLATFORM_OVERVIEW.md +194 -0
  29. package/docs/QUICKSTART.md +252 -0
  30. package/docs/QUICK_REFERENCE.md +222 -0
  31. package/docs/READY_TO_TEST.md +383 -0
  32. package/docs/RUN.md +237 -0
  33. package/docs/START_TESTING.md +130 -0
  34. package/docs/STRIPE_CONNECT_SETUP.md +454 -0
  35. package/docs/SYSTEM_READY.md +345 -0
  36. package/docs/THREADS_IMPLEMENTATION_COMPLETE.md +431 -0
  37. package/docs/THREAD_IMPLEMENTATION_GUIDE.md +345 -0
  38. package/docs/firebasefiles.md +21 -0
  39. package/docs/homepage.md +1208 -0
  40. package/docs/payments.md +159 -0
  41. package/eslint.config.js +76 -0
  42. package/firebase.json +27 -0
  43. package/firestore.indexes.json +88 -0
  44. package/firestore.rules +125 -0
  45. package/functions/package-lock.json +2884 -0
  46. package/functions/package.json +20 -0
  47. package/functions/src/analytics.js +101 -0
  48. package/functions/src/audit.js +28 -0
  49. package/functions/src/auth.js +19 -0
  50. package/functions/src/drafts.js +128 -0
  51. package/functions/src/feedback.js +164 -0
  52. package/functions/src/firebase-init.js +29 -0
  53. package/functions/src/i18n.js +134 -0
  54. package/functions/src/limits.js +27 -0
  55. package/functions/src/llm.js +271 -0
  56. package/functions/src/ner.js +162 -0
  57. package/functions/src/notifications.js +273 -0
  58. package/functions/src/orgs.js +127 -0
  59. package/functions/src/patient-api.js +545 -0
  60. package/functions/src/payment.js +830 -0
  61. package/functions/src/safety.js +188 -0
  62. package/functions/src/stripe-config.js +33 -0
  63. package/functions/src/topics.js +94 -0
  64. package/functions/src/triage.js +191 -0
  65. package/functions/src/utils.js +28 -0
  66. package/functions/src/validate.js +177 -0
  67. package/functions/test-firebase.js +36 -0
  68. package/jest.config.js +18 -0
  69. package/package.json +52 -0
  70. package/public/404.html +93 -0
  71. package/public/app/analytics.html +336 -0
  72. package/public/app/analytics.js +177 -0
  73. package/public/app/app.css +1400 -0
  74. package/public/app/app.js +1754 -0
  75. package/public/app/config.js +14 -0
  76. package/public/app/inbox-threads.js +843 -0
  77. package/public/app/inbox.html +598 -0
  78. package/public/app/inbox.js +397 -0
  79. package/public/app/index.html +612 -0
  80. package/public/assets/logo.jpg +0 -0
  81. package/public/chat.html +137 -0
  82. package/public/css/chat.css +870 -0
  83. package/public/css/landing-enhance.css +298 -0
  84. package/public/css/styles.css +1198 -0
  85. package/public/favicon.svg +5 -0
  86. package/public/find-doctor.html +220 -0
  87. package/public/index.html +399 -0
  88. package/public/js/chat-threads.js +513 -0
  89. package/public/js/chat.js +832 -0
  90. package/public/js/search.js +41 -0
  91. package/public/privacy.html +101 -0
  92. package/public/robots.txt +6 -0
  93. package/public/site.webmanifest +12 -0
  94. package/public/terms.html +110 -0
  95. package/scripts/create-test-provider.js +55 -0
  96. package/scripts/deploy-staging.sh +51 -0
  97. package/scripts/fix-provider.js +39 -0
  98. package/scripts/quick-test.sh +37 -0
  99. package/scripts/test-chat.sh +54 -0
  100. package/scripts/test-multi-turn.js +137 -0
  101. package/scripts/test-ner-simple.js +28 -0
  102. package/scripts/test-ner.js +60 -0
  103. package/scripts/test-stripe-connect.js +304 -0
  104. package/scripts/test-webhook-secret.sh +24 -0
  105. package/server.js +3864 -0
  106. package/storage.rules +22 -0
  107. package/test-api.json +1 -0
  108. package/tests/emulator/README.md +38 -0
  109. package/tests/emulator/counters.emulator.test.js +81 -0
  110. package/tests/integration/api-account-selfserve.test.js +312 -0
  111. package/tests/integration/api-admin-digest.test.js +202 -0
  112. package/tests/integration/api-chat-satisfaction.test.js +259 -0
  113. package/tests/integration/api-doctor-drafts.test.js +236 -0
  114. package/tests/integration/api-doctor-inbox.test.js +401 -0
  115. package/tests/integration/api-org.test.js +328 -0
  116. package/tests/integration/api-payment.test.js +664 -0
  117. package/tests/integration/api-provider-settings.test.js +475 -0
  118. package/tests/integration/api-qr.test.js +121 -0
  119. package/tests/integration/api-security.test.js +241 -0
  120. package/tests/integration/api-stripe-search.test.js +303 -0
  121. package/tests/integration/api-webhook-deletion.test.js +231 -0
  122. package/tests/mocks/firebase-admin.js +220 -0
  123. package/tests/unit/analytics.test.js +151 -0
  124. package/tests/unit/audit-bugs.test.js +203 -0
  125. package/tests/unit/chat-ui-logic.test.js +283 -0
  126. package/tests/unit/drafts.test.js +104 -0
  127. package/tests/unit/feedback.test.js +148 -0
  128. package/tests/unit/i18n.test.js +229 -0
  129. package/tests/unit/llm-retry.test.js +122 -0
  130. package/tests/unit/notifications.test.js +155 -0
  131. package/tests/unit/orgs.test.js +328 -0
  132. package/tests/unit/payment.test.js +889 -0
  133. package/tests/unit/safety-edge-cases.test.js +509 -0
  134. package/tests/unit/safety-srs-compliance.test.js +252 -0
  135. package/tests/unit/safety.test.js +291 -0
  136. package/tests/unit/server-helpers.test.js +774 -0
  137. package/tests/unit/topics-limits.test.js +158 -0
  138. package/tests/unit/triage.test.js +152 -0
  139. package/tests/unit/validate.test.js +408 -0
@@ -0,0 +1,1208 @@
1
+ Excellent idea! This transforms EvalDoc from a back-end API into a **complete patient-facing platform**. Here's my assessment and implementation strategy:
2
+
3
+ ## 🎯 **Strategic Value**
4
+
5
+ **BENEFITS:**
6
+ 1. **Direct patient access** - No need for providers to share custom URLs
7
+ 2. **Professional appearance** - Creates trust and legitimacy
8
+ 3. **Provider directory** - Patients can find their doctor
9
+ 4. **Scalable onboarding** - Easy for new providers to join
10
+ 5. **Brand building** - Creates a recognizable healthcare platform
11
+
12
+ ## 🏗️ **Recommended Architecture**
13
+
14
+ ```
15
+ ┌─────────────────────────────────────┐
16
+ │ EvalDoc Homepage │
17
+ │ ┌─────────────────────────────┐ │
18
+ │ │ 1. Hero Section │ │
19
+ │ │ 2. Provider Search │ │
20
+ │ │ 3. How It Works │ │
21
+ │ │ 4. Login/Signup │ │
22
+ │ └─────────────────────────────┘ │
23
+ └─────────────────────────────────────┘
24
+ │
25
+ ▼
26
+ ┌─────────────────────────────────────┐
27
+ │ Provider Search Results │
28
+ │ ┌─────────────────────────────┐ │
29
+ │ │ • Search by name/specialty │ │
30
+ │ │ • Filter by location │ │
31
+ │ │ • Verified badges │ │
32
+ │ └─────────────────────────────┘ │
33
+ └─────────────────────────────────────┘
34
+ │
35
+ ▼
36
+ ┌─────────────────────────────────────┐
37
+ │ Provider Chat Interface │
38
+ │ ┌─────────────────────────────┐ │
39
+ │ │ • Direct messaging │ │
40
+ │ │ • Safety warnings │ │
41
+ │ │ • File upload (optional) │ │
42
+ │ │ • Appointment link │ │
43
+ │ └─────────────────────────────┘ │
44
+ └─────────────────────────────────────┘
45
+ ```
46
+
47
+ ## 🚀 **Implementation Plan**
48
+
49
+ ### **Phase 1: Homepage & Provider Search (Week 1)**
50
+
51
+ **File Structure:**
52
+ ```
53
+ public/
54
+ ├── index.html # Homepage
55
+ ├── app.html # Chat interface
56
+ ├── find-doctor.html # Provider search
57
+ ├── provider.html # Provider profile
58
+ ├── css/
59
+ │ ├── styles.css # Main styles
60
+ │ └── chat.css # Chat-specific styles
61
+ ├── js/
62
+ │ ├── app.js # Main application
63
+ │ ├── search.js # Provider search
64
+ │ └── chat.js # Chat functionality
65
+ └── assets/
66
+ ├── logos/
67
+ └── illustrations/
68
+ ```
69
+
70
+ **Homepage (index.html) - Key Sections:**
71
+ ```html
72
+ <!DOCTYPE html>
73
+ <html lang="en">
74
+ <head>
75
+ <meta charset="UTF-8">
76
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
77
+ <title>EvalDoc - AI-Powered Patient Support</title>
78
+ <link rel="stylesheet" href="/css/styles.css">
79
+ </head>
80
+ <body>
81
+ <!-- Navigation -->
82
+ <nav class="navbar">
83
+ <div class="nav-container">
84
+ <a href="/" class="logo">
85
+ <img src="/assets/logo.svg" alt="EvalDoc">
86
+ <span>EvalDoc</span>
87
+ </a>
88
+ <div class="nav-links">
89
+ <a href="/find-doctor">Find Your Doctor</a>
90
+ <a href="#how-it-works">How It Works</a>
91
+ <a href="#safety">Safety</a>
92
+ <a href="/login" class="btn-outline">Provider Login</a>
93
+ </div>
94
+ </div>
95
+ </nav>
96
+
97
+ <!-- Hero Section -->
98
+ <section class="hero">
99
+ <div class="hero-container">
100
+ <div class="hero-text">
101
+ <h1>Connect with your healthcare provider safely</h1>
102
+ <p>AI-powered patient support with medical safety guards. Get answers to non-urgent questions before your appointment.</p>
103
+
104
+ <!-- Provider Search Box -->
105
+ <div class="search-box">
106
+ <h3>Find your provider</h3>
107
+ <div class="search-form">
108
+ <input type="text"
109
+ id="providerSearch"
110
+ placeholder="Enter your doctor's name or specialty...">
111
+ <button onclick="searchProvider()">Find</button>
112
+ </div>
113
+ <p class="search-hint">Start typing to find your healthcare provider</p>
114
+
115
+ <!-- Quick Results -->
116
+ <div id="quickResults" class="quick-results"></div>
117
+ </div>
118
+ </div>
119
+ <div class="hero-image">
120
+ <img src="/assets/hero-illustration.svg" alt="Doctor consultation">
121
+ </div>
122
+ </div>
123
+ </section>
124
+
125
+ <!-- How It Works -->
126
+ <section id="how-it-works" class="section">
127
+ <h2>How EvalDoc Works</h2>
128
+ <div class="steps">
129
+ <div class="step">
130
+ <div class="step-number">1</div>
131
+ <h3>Find Your Provider</h3>
132
+ <p>Search for your doctor by name or specialty</p>
133
+ </div>
134
+ <div class="step">
135
+ <div class="step-number">2</div>
136
+ <h3>Ask Questions Safely</h3>
137
+ <p>Get AI-powered responses with medical safety checks</p>
138
+ </div>
139
+ <div class="step">
140
+ <div class="step-number">3</div>
141
+ <h3>Provider Review</h3>
142
+ <p>Your doctor reviews all conversations for follow-up</p>
143
+ </div>
144
+ </div>
145
+ </section>
146
+
147
+ <!-- Safety Features -->
148
+ <section id="safety" class="section dark">
149
+ <h2>Built with Medical Safety First</h2>
150
+ <div class="safety-features">
151
+ <div class="feature">
152
+ <div class="feature-icon">⚠️</div>
153
+ <h3>No Diagnoses</h3>
154
+ <p>AI never provides medical diagnoses</p>
155
+ </div>
156
+ <div class="feature">
157
+ <div class="feature-icon">🚨</div>
158
+ <h3>Urgent Symptom Detection</h3>
159
+ <p>Auto-escalates chest pain, breathing issues, etc.</p>
160
+ </div>
161
+ <div class="feature">
162
+ <div class="feature-icon">🔒</div>
163
+ <h3>HIPAA-Compliant</h3>
164
+ <p>PII detection and secure data handling</p>
165
+ </div>
166
+ <div class="feature">
167
+ <div class="feature-icon">👁️</div>
168
+ <h3>Provider Oversight</h3>
169
+ <p>All conversations reviewed by your doctor</p>
170
+ </div>
171
+ </div>
172
+ </section>
173
+
174
+ <!-- Provider Signup CTA -->
175
+ <section class="section cta">
176
+ <div class="cta-container">
177
+ <h2>Are you a healthcare provider?</h2>
178
+ <p>Join EvalDoc to offer AI-powered patient support for your practice.</p>
179
+ <a href="/provider-signup" class="btn-primary">Sign Up as Provider</a>
180
+ </div>
181
+ </section>
182
+
183
+ <script src="/js/search.js"></script>
184
+ </body>
185
+ </html>
186
+ ```
187
+
188
+ ### **Phase 2: Provider Search & Profile (Week 1)**
189
+
190
+ **Provider Search Implementation:**
191
+ ```javascript
192
+ // public/js/search.js
193
+
194
+ class ProviderSearch {
195
+ constructor() {
196
+ this.providers = [];
197
+ this.init();
198
+ }
199
+
200
+ async init() {
201
+ // Load providers from API
202
+ await this.loadProviders();
203
+ this.setupSearch();
204
+ }
205
+
206
+ async loadProviders() {
207
+ try {
208
+ const response = await fetch('/api/providers');
209
+ this.providers = await response.json();
210
+ console.log(`Loaded ${this.providers.length} providers`);
211
+ } catch (error) {
212
+ console.error('Error loading providers:', error);
213
+ this.providers = this.getDemoProviders();
214
+ }
215
+ }
216
+
217
+ setupSearch() {
218
+ const searchInput = document.getElementById('providerSearch');
219
+ if (!searchInput) return;
220
+
221
+ searchInput.addEventListener('input', (e) => {
222
+ this.performSearch(e.target.value);
223
+ });
224
+
225
+ searchInput.addEventListener('keypress', (e) => {
226
+ if (e.key === 'Enter') {
227
+ this.performSearch(e.target.value, true);
228
+ }
229
+ });
230
+ }
231
+
232
+ performSearch(query, redirect = false) {
233
+ if (!query.trim()) {
234
+ this.clearResults();
235
+ return;
236
+ }
237
+
238
+ const results = this.searchProviders(query);
239
+ this.displayResults(results);
240
+
241
+ if (redirect && results.length === 1) {
242
+ // Auto-redirect if only one match
243
+ this.redirectToProvider(results[0].handle);
244
+ }
245
+ }
246
+
247
+ searchProviders(query) {
248
+ const searchTerm = query.toLowerCase();
249
+
250
+ return this.providers.filter(provider => {
251
+ return (
252
+ provider.displayName.toLowerCase().includes(searchTerm) ||
253
+ provider.specialty.toLowerCase().includes(searchTerm) ||
254
+ (provider.keywords || []).some(kw =>
255
+ kw.toLowerCase().includes(searchTerm)
256
+ )
257
+ );
258
+ }).slice(0, 10); // Limit results
259
+ }
260
+
261
+ displayResults(results) {
262
+ const container = document.getElementById('quickResults');
263
+ if (!container) return;
264
+
265
+ if (results.length === 0) {
266
+ container.innerHTML = `
267
+ <div class="no-results">
268
+ <p>No providers found. Try a different search term.</p>
269
+ <p class="small">Providers: Dr. Smith (Cardiology), Dr. Johnson (Pediatrics), etc.</p>
270
+ </div>
271
+ `;
272
+ return;
273
+ }
274
+
275
+ container.innerHTML = results.map(provider => `
276
+ <div class="provider-result" onclick="window.location='/provider/${provider.handle}'">
277
+ <div class="provider-info">
278
+ <h4>${provider.displayName}</h4>
279
+ <p class="specialty">${provider.specialty}</p>
280
+ <p class="location">${provider.location || 'Online'}</p>
281
+ </div>
282
+ <div class="provider-actions">
283
+ <button class="btn-small" onclick="event.stopPropagation(); window.location='/app?provider=${provider.handle}'">
284
+ Start Chat
285
+ </button>
286
+ </div>
287
+ </div>
288
+ `).join('');
289
+ }
290
+
291
+ clearResults() {
292
+ const container = document.getElementById('quickResults');
293
+ if (container) container.innerHTML = '';
294
+ }
295
+
296
+ redirectToProvider(handle) {
297
+ window.location.href = `/provider/${handle}`;
298
+ }
299
+
300
+ getDemoProviders() {
301
+ return [
302
+ {
303
+ handle: 'tyrone-norman',
304
+ displayName: 'Dr. Tyrone Norman',
305
+ specialty: 'General Practice',
306
+ location: 'New York, NY',
307
+ verified: true,
308
+ keywords: ['primary care', 'family medicine', 'gp']
309
+ },
310
+ {
311
+ handle: 'sarah-chen',
312
+ displayName: 'Dr. Sarah Chen',
313
+ specialty: 'Cardiology',
314
+ location: 'San Francisco, CA',
315
+ verified: true,
316
+ keywords: ['heart', 'cardiac', 'echocardiogram']
317
+ },
318
+ {
319
+ handle: 'michael-rodriguez',
320
+ displayName: 'Dr. Michael Rodriguez',
321
+ specialty: 'Pediatrics',
322
+ location: 'Miami, FL',
323
+ verified: true,
324
+ keywords: ['children', 'pediatric', 'kids']
325
+ }
326
+ ];
327
+ }
328
+ }
329
+
330
+ // Initialize search
331
+ const providerSearch = new ProviderSearch();
332
+
333
+ // Global function for button click
334
+ function searchProvider() {
335
+ const input = document.getElementById('providerSearch');
336
+ if (input && input.value.trim()) {
337
+ window.location.href = `/find-doctor?q=${encodeURIComponent(input.value)}`;
338
+ }
339
+ }
340
+ ```
341
+
342
+ ### **Phase 3: Enhanced Chat Interface (Week 2)**
343
+
344
+ **Simplified Chat Access:**
345
+ ```javascript
346
+ // public/js/chat.js
347
+
348
+ class PatientChat {
349
+ constructor() {
350
+ this.providerHandle = this.getProviderFromURL();
351
+ this.sessionId = this.generateSessionId();
352
+ this.conversationId = null;
353
+ this.init();
354
+ }
355
+
356
+ init() {
357
+ if (!this.providerHandle) {
358
+ this.showProviderSelector();
359
+ return;
360
+ }
361
+
362
+ this.loadProviderInfo();
363
+ this.setupChat();
364
+ }
365
+
366
+ getProviderFromURL() {
367
+ const urlParams = new URLSearchParams(window.location.search);
368
+ return urlParams.get('provider') ||
369
+ window.location.pathname.split('/').pop();
370
+ }
371
+
372
+ showProviderSelector() {
373
+ document.getElementById('chatContainer').innerHTML = `
374
+ <div class="provider-selector">
375
+ <h2>Chat with Your Provider</h2>
376
+ <p>Enter your provider's username or find them in our directory</p>
377
+
378
+ <div class="provider-input">
379
+ <input type="text"
380
+ id="providerHandleInput"
381
+ placeholder="e.g., 'tyrone-norman'">
382
+ <button onclick="connectToProvider()">Connect</button>
383
+ </div>
384
+
385
+ <p class="small">Don't know your provider's username?
386
+ <a href="/find-doctor">Find your provider</a>
387
+ </p>
388
+
389
+ <div class="recent-providers">
390
+ <h4>Recent Providers</h4>
391
+ <!-- Dynamically loaded -->
392
+ </div>
393
+ </div>
394
+ `;
395
+ }
396
+
397
+ async loadProviderInfo() {
398
+ try {
399
+ const response = await fetch(`/api/providers/${this.providerHandle}`);
400
+ const provider = await response.json();
401
+
402
+ document.getElementById('providerName').textContent =
403
+ provider.displayName || this.providerHandle;
404
+ document.getElementById('providerSpecialty').textContent =
405
+ provider.specialty || 'Healthcare Provider';
406
+
407
+ // Update page title
408
+ document.title = `Chat with ${provider.displayName} - EvalDoc`;
409
+ } catch (error) {
410
+ console.error('Error loading provider info:', error);
411
+ }
412
+ }
413
+
414
+ setupChat() {
415
+ // Initialize chat UI
416
+ this.renderChatInterface();
417
+ this.setupMessageHandlers();
418
+ this.loadConversationHistory();
419
+ }
420
+
421
+ renderChatInterface() {
422
+ document.getElementById('chatContainer').innerHTML = `
423
+ <div class="chat-wrapper">
424
+ <!-- Chat Header -->
425
+ <div class="chat-header">
426
+ <div class="provider-badge">
427
+ <div class="avatar">${this.getInitials()}</div>
428
+ <div class="provider-info">
429
+ <h3 id="providerName">${this.providerHandle}</h3>
430
+ <p id="providerSpecialty">Loading...</p>
431
+ </div>
432
+ </div>
433
+ <div class="chat-actions">
434
+ <button class="btn-icon" title="Safety Info" onclick="showSafetyInfo()">
435
+ ⚕️
436
+ </button>
437
+ <button class="btn-icon" title="End Chat" onclick="endChat()">
438
+ ✕
439
+ </button>
440
+ </div>
441
+ </div>
442
+
443
+ <!-- Chat Messages -->
444
+ <div class="chat-messages" id="chatMessages">
445
+ <div class="welcome-message">
446
+ <p>You're now chatting with AI-assisted support for
447
+ <strong id="dynamicProviderName">${this.providerHandle}</strong>.
448
+ </p>
449
+ <p class="safety-note">
450
+ ⚠️ <strong>Important:</strong> This AI provides educational
451
+ information only. It cannot diagnose, prescribe, or provide
452
+ emergency care.
453
+ </p>
454
+ </div>
455
+ </div>
456
+
457
+ <!-- Message Input -->
458
+ <div class="message-input">
459
+ <textarea
460
+ id="messageInput"
461
+ placeholder="Type your non-urgent health question..."
462
+ rows="1"></textarea>
463
+ <button id="sendButton" onclick="sendMessage()">Send</button>
464
+ </div>
465
+
466
+ <!-- Safety Footer -->
467
+ <div class="safety-footer">
468
+ <p>
469
+ <span class="urgent-warning">🚨 Urgent symptoms?</span>
470
+ Call 911 or go to emergency room immediately.
471
+ </p>
472
+ <p class="disclaimer">
473
+ All conversations are reviewed by your healthcare provider.
474
+ </p>
475
+ </div>
476
+ </div>
477
+ `;
478
+ }
479
+
480
+ async sendMessage() {
481
+ const input = document.getElementById('messageInput');
482
+ const message = input.value.trim();
483
+
484
+ if (!message) return;
485
+
486
+ // Disable input during processing
487
+ input.disabled = true;
488
+
489
+ // Add user message to chat
490
+ this.addMessage('user', message);
491
+ input.value = '';
492
+
493
+ try {
494
+ const response = await fetch('/api/chat', {
495
+ method: 'POST',
496
+ headers: { 'Content-Type': 'application/json' },
497
+ body: JSON.stringify({
498
+ handle: this.providerHandle,
499
+ sessionId: this.sessionId,
500
+ message: message,
501
+ conversationId: this.conversationId
502
+ })
503
+ });
504
+
505
+ const data = await response.json();
506
+
507
+ // Store conversation ID for continuity
508
+ if (data.conversationId && !this.conversationId) {
509
+ this.conversationId = data.conversationId;
510
+ }
511
+
512
+ // Add AI response to chat
513
+ this.addMessage('ai', data.response, {
514
+ escalation: data.escalation,
515
+ safetyFlags: data.safetyFlags
516
+ });
517
+
518
+ // Handle escalation
519
+ if (data.escalation) {
520
+ this.showEscalationAlert();
521
+ }
522
+
523
+ } catch (error) {
524
+ console.error('Error sending message:', error);
525
+ this.addMessage('system',
526
+ 'Sorry, there was an error. Please try again or contact your provider directly.');
527
+ } finally {
528
+ input.disabled = false;
529
+ input.focus();
530
+ }
531
+ }
532
+
533
+ addMessage(sender, text, metadata = {}) {
534
+ const messagesDiv = document.getElementById('chatMessages');
535
+ const messageDiv = document.createElement('div');
536
+
537
+ messageDiv.className = `message ${sender}-message`;
538
+ if (metadata.escalation) {
539
+ messageDiv.classList.add('escalated');
540
+ }
541
+
542
+ messageDiv.innerHTML = `
543
+ <div class="message-content">
544
+ ${text}
545
+ </div>
546
+ <div class="message-meta">
547
+ <span class="timestamp">${new Date().toLocaleTimeString([],
548
+ {hour: '2-digit', minute:'2-digit'})}</span>
549
+ ${metadata.escalation ? '<span class="escalation-badge">URGENT</span>' : ''}
550
+ </div>
551
+ `;
552
+
553
+ messagesDiv.appendChild(messageDiv);
554
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
555
+ }
556
+
557
+ generateSessionId() {
558
+ return 'patient-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
559
+ }
560
+
561
+ getInitials() {
562
+ const parts = this.providerHandle.split('-');
563
+ return parts.map(p => p.charAt(0).toUpperCase()).join('');
564
+ }
565
+
566
+ showEscalationAlert() {
567
+ const alertDiv = document.createElement('div');
568
+ alertDiv.className = 'escalation-alert';
569
+ alertDiv.innerHTML = `
570
+ <div class="alert-content">
571
+ <h4>⚠️ Urgent Symptoms Detected</h4>
572
+ <p>Your message contained symptoms that may require immediate attention.</p>
573
+ <p><strong>Please:</strong></p>
574
+ <ul>
575
+ <li>Contact your provider directly</li>
576
+ <li>Call 911 for life-threatening symptoms</li>
577
+ <li>Go to the nearest emergency room if needed</li>
578
+ </ul>
579
+ <button onclick="this.parentElement.parentElement.remove()">
580
+ I Understand
581
+ </button>
582
+ </div>
583
+ `;
584
+
585
+ document.body.appendChild(alertDiv);
586
+ }
587
+ }
588
+
589
+ // Initialize chat when page loads
590
+ let chatApp;
591
+ document.addEventListener('DOMContentLoaded', () => {
592
+ chatApp = new PatientChat();
593
+ });
594
+
595
+ // Global functions for button clicks
596
+ function sendMessage() {
597
+ if (chatApp) chatApp.sendMessage();
598
+ }
599
+
600
+ function connectToProvider() {
601
+ const input = document.getElementById('providerHandleInput');
602
+ if (input && input.value.trim()) {
603
+ window.location.href = `/app?provider=${encodeURIComponent(input.value)}`;
604
+ }
605
+ }
606
+ ```
607
+
608
+ ### **Phase 4: Server-Side API Extensions (Week 2)**
609
+
610
+ **Extend your server.js:**
611
+ ```javascript
612
+ // Add new API endpoints
613
+
614
+ // Get all providers (for search)
615
+ app.get('/api/providers', async (req, res) => {
616
+ try {
617
+ const providersRef = db.collection('providers')
618
+ .where('status', '==', 'active')
619
+ .limit(100);
620
+
621
+ const snapshot = await providersRef.get();
622
+ const providers = [];
623
+
624
+ snapshot.forEach(doc => {
625
+ providers.push({
626
+ id: doc.id,
627
+ handle: doc.data().handle,
628
+ displayName: doc.data().displayName,
629
+ specialty: doc.data().specialty,
630
+ location: doc.data().location,
631
+ verified: doc.data().verified || false,
632
+ keywords: doc.data().keywords || []
633
+ });
634
+ });
635
+
636
+ res.json(providers);
637
+ } catch (error) {
638
+ console.error('Error fetching providers:', error);
639
+ res.status(500).json({ error: 'Failed to fetch providers' });
640
+ }
641
+ });
642
+
643
+ // Get specific provider
644
+ app.get('/api/providers/:handle', async (req, res) => {
645
+ try {
646
+ const snapshot = await db.collection('providers')
647
+ .where('handle', '==', req.params.handle)
648
+ .limit(1)
649
+ .get();
650
+
651
+ if (snapshot.empty) {
652
+ return res.status(404).json({ error: 'Provider not found' });
653
+ }
654
+
655
+ const doc = snapshot.docs[0];
656
+ res.json({
657
+ id: doc.id,
658
+ ...doc.data()
659
+ });
660
+ } catch (error) {
661
+ console.error('Error fetching provider:', error);
662
+ res.status(500).json({ error: 'Failed to fetch provider' });
663
+ }
664
+ });
665
+
666
+ // Provider signup endpoint
667
+ app.post('/api/providers/signup', async (req, res) => {
668
+ try {
669
+ const {
670
+ email,
671
+ displayName,
672
+ specialty,
673
+ licenseNumber,
674
+ npiNumber
675
+ } = req.body;
676
+
677
+ // Validate required fields
678
+ if (!email || !displayName || !specialty) {
679
+ return res.status(400).json({ error: 'Missing required fields' });
680
+ }
681
+
682
+ // Check if provider already exists
683
+ const existing = await db.collection('providers')
684
+ .where('email', '==', email)
685
+ .limit(1)
686
+ .get();
687
+
688
+ if (!existing.empty) {
689
+ return res.status(409).json({ error: 'Provider already exists' });
690
+ }
691
+
692
+ // Generate unique handle
693
+ const handle = displayName.toLowerCase()
694
+ .replace(/[^a-z0-9]/g, '-')
695
+ .replace(/-+/g, '-')
696
+ .replace(/^-|-$/g, '');
697
+
698
+ // Create provider document
699
+ const providerData = {
700
+ email,
701
+ displayName,
702
+ handle,
703
+ specialty,
704
+ licenseNumber: licenseNumber || '',
705
+ npiNumber: npiNumber || '',
706
+ status: 'pending', // Needs admin approval
707
+ createdAt: admin.firestore.FieldValue.serverTimestamp(),
708
+ updatedAt: admin.firestore.FieldValue.serverTimestamp()
709
+ };
710
+
711
+ const docRef = await db.collection('providers').add(providerData);
712
+
713
+ res.json({
714
+ success: true,
715
+ providerId: docRef.id,
716
+ handle: handle,
717
+ message: 'Provider registration submitted for approval'
718
+ });
719
+
720
+ } catch (error) {
721
+ console.error('Error in provider signup:', error);
722
+ res.status(500).json({ error: 'Registration failed' });
723
+ }
724
+ });
725
+
726
+ // Serve static files (add to your server.js)
727
+ app.use(express.static('public'));
728
+
729
+ // Route for homepage
730
+ app.get('/', (req, res) => {
731
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
732
+ });
733
+
734
+ // Route for chat interface
735
+ app.get('/app', (req, res) => {
736
+ res.sendFile(path.join(__dirname, 'public', 'app.html'));
737
+ });
738
+
739
+ // Route for provider search
740
+ app.get('/find-doctor', (req, res) => {
741
+ res.sendFile(path.join(__dirname, 'public', 'find-doctor.html'));
742
+ });
743
+
744
+ // Route for provider profile
745
+ app.get('/provider/:handle', (req, res) => {
746
+ res.sendFile(path.join(__dirname, 'public', 'provider.html'));
747
+ });
748
+ ```
749
+
750
+ ### **Phase 5: Database Schema Updates**
751
+
752
+ **Firestore Collections:**
753
+
754
+ ```javascript
755
+ // providers collection
756
+ {
757
+ "handle": "tyrone-norman", // URL-friendly ID
758
+ "displayName": "Dr. Tyrone Norman",
759
+ "email": "dr.norman@clinic.com",
760
+ "specialty": "General Practice",
761
+ "location": "New York, NY",
762
+ "licenseNumber": "NY123456",
763
+ "npiNumber": "1234567890",
764
+ "verified": true,
765
+ "status": "active", // active, pending, suspended
766
+ "keywords": ["primary care", "family medicine", "gp"],
767
+ "profileImage": "https://...", // Optional
768
+ "clinicHours": "Mon-Fri 9am-5pm", // Optional
769
+ "phone": "+1-555-123-4567", // Optional
770
+ "website": "https://clinic.com", // Optional
771
+ "createdAt": timestamp,
772
+ "updatedAt": timestamp
773
+ }
774
+
775
+ // provider_preferences collection (subcollection under providers)
776
+ {
777
+ "safetyLevel": "high",
778
+ "customGreeting": "Hello, I'm Dr. Norman's AI assistant...",
779
+ "escalationContacts": ["nurse@clinic.com", "555-123-4567"],
780
+ "autoReplyHours": "9am-5pm",
781
+ "blockedTerms": ["experimental", "unproven"],
782
+ "preferredTopics": ["appointment prep", "medication info"],
783
+ "updatedAt": timestamp
784
+ }
785
+
786
+ // patient_sessions collection
787
+ {
788
+ "sessionId": "patient-123456789",
789
+ "providerId": "providerDocId",
790
+ "providerHandle": "tyrone-norman",
791
+ "patientId": null, // Anonymous by default
792
+ "patientEmail": null, // Optional if provided
793
+ "conversationId": "conv123",
794
+ "startedAt": timestamp,
795
+ "lastActivity": timestamp,
796
+ "status": "active", // active, completed, abandoned
797
+ "escalationCount": 0,
798
+ "messageCount": 0,
799
+ "safetyEvents": ["medical_entities_detected"],
800
+ "metadata": {
801
+ "userAgent": "Mozilla/5.0...",
802
+ "ip": "192.168.1.1", // Hashed for privacy
803
+ "referrer": "https://evaldoc.com/find-doctor"
804
+ }
805
+ }
806
+ ```
807
+
808
+ ### **Phase 6: CSS/UI Styling (Week 2)**
809
+
810
+ **public/css/styles.css:**
811
+ ```css
812
+ /* Base Styles */
813
+ :root {
814
+ --primary: #0066cc;
815
+ --primary-dark: #0052a3;
816
+ --secondary: #00a896;
817
+ --danger: #e63946;
818
+ --warning: #ff9f1c;
819
+ --success: #2a9d8f;
820
+ --light: #f8f9fa;
821
+ --dark: #212529;
822
+ --gray: #6c757d;
823
+ --border: #dee2e6;
824
+ }
825
+
826
+ * {
827
+ margin: 0;
828
+ padding: 0;
829
+ box-sizing: border-box;
830
+ }
831
+
832
+ body {
833
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
834
+ line-height: 1.6;
835
+ color: var(--dark);
836
+ }
837
+
838
+ /* Navigation */
839
+ .navbar {
840
+ background: white;
841
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
842
+ position: sticky;
843
+ top: 0;
844
+ z-index: 1000;
845
+ }
846
+
847
+ .nav-container {
848
+ max-width: 1200px;
849
+ margin: 0 auto;
850
+ padding: 1rem 2rem;
851
+ display: flex;
852
+ justify-content: space-between;
853
+ align-items: center;
854
+ }
855
+
856
+ .logo {
857
+ display: flex;
858
+ align-items: center;
859
+ gap: 0.5rem;
860
+ text-decoration: none;
861
+ font-size: 1.5rem;
862
+ font-weight: bold;
863
+ color: var(--primary);
864
+ }
865
+
866
+ .nav-links {
867
+ display: flex;
868
+ gap: 2rem;
869
+ align-items: center;
870
+ }
871
+
872
+ .nav-links a {
873
+ text-decoration: none;
874
+ color: var(--dark);
875
+ transition: color 0.2s;
876
+ }
877
+
878
+ .nav-links a:hover {
879
+ color: var(--primary);
880
+ }
881
+
882
+ .btn-outline {
883
+ border: 2px solid var(--primary);
884
+ color: var(--primary);
885
+ padding: 0.5rem 1.5rem;
886
+ border-radius: 50px;
887
+ text-decoration: none;
888
+ font-weight: 600;
889
+ transition: all 0.2s;
890
+ }
891
+
892
+ .btn-outline:hover {
893
+ background: var(--primary);
894
+ color: white;
895
+ }
896
+
897
+ /* Hero Section */
898
+ .hero {
899
+ padding: 4rem 2rem;
900
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
901
+ }
902
+
903
+ .hero-container {
904
+ max-width: 1200px;
905
+ margin: 0 auto;
906
+ display: grid;
907
+ grid-template-columns: 1fr 1fr;
908
+ gap: 4rem;
909
+ align-items: center;
910
+ }
911
+
912
+ .hero-text h1 {
913
+ font-size: 3rem;
914
+ margin-bottom: 1.5rem;
915
+ color: var(--dark);
916
+ }
917
+
918
+ .hero-text p {
919
+ font-size: 1.2rem;
920
+ color: var(--gray);
921
+ margin-bottom: 2rem;
922
+ }
923
+
924
+ /* Search Box */
925
+ .search-box {
926
+ background: white;
927
+ padding: 2rem;
928
+ border-radius: 16px;
929
+ box-shadow: 0 10px 30px rgba(0,0,0,0.1);
930
+ margin-top: 2rem;
931
+ }
932
+
933
+ .search-form {
934
+ display: flex;
935
+ gap: 1rem;
936
+ margin: 1rem 0;
937
+ }
938
+
939
+ .search-form input {
940
+ flex: 1;
941
+ padding: 1rem;
942
+ border: 2px solid var(--border);
943
+ border-radius: 8px;
944
+ font-size: 1rem;
945
+ transition: border-color 0.2s;
946
+ }
947
+
948
+ .search-form input:focus {
949
+ outline: none;
950
+ border-color: var(--primary);
951
+ }
952
+
953
+ .search-form button {
954
+ background: var(--primary);
955
+ color: white;
956
+ border: none;
957
+ padding: 0 2rem;
958
+ border-radius: 8px;
959
+ font-weight: 600;
960
+ cursor: pointer;
961
+ transition: background 0.2s;
962
+ }
963
+
964
+ .search-form button:hover {
965
+ background: var(--primary-dark);
966
+ }
967
+
968
+ /* Quick Results */
969
+ .quick-results {
970
+ margin-top: 1rem;
971
+ max-height: 300px;
972
+ overflow-y: auto;
973
+ }
974
+
975
+ .provider-result {
976
+ display: flex;
977
+ justify-content: space-between;
978
+ align-items: center;
979
+ padding: 1rem;
980
+ border: 1px solid var(--border);
981
+ border-radius: 8px;
982
+ margin-bottom: 0.5rem;
983
+ cursor: pointer;
984
+ transition: all 0.2s;
985
+ }
986
+
987
+ .provider-result:hover {
988
+ border-color: var(--primary);
989
+ background: #f8f9fa;
990
+ }
991
+
992
+ .provider-info h4 {
993
+ margin-bottom: 0.25rem;
994
+ }
995
+
996
+ .specialty {
997
+ color: var(--gray);
998
+ font-size: 0.9rem;
999
+ }
1000
+
1001
+ .btn-small {
1002
+ padding: 0.5rem 1rem;
1003
+ background: var(--primary);
1004
+ color: white;
1005
+ border: none;
1006
+ border-radius: 6px;
1007
+ cursor: pointer;
1008
+ font-size: 0.9rem;
1009
+ }
1010
+
1011
+ /* Chat Interface */
1012
+ .chat-wrapper {
1013
+ max-width: 800px;
1014
+ margin: 0 auto;
1015
+ height: 100vh;
1016
+ display: flex;
1017
+ flex-direction: column;
1018
+ }
1019
+
1020
+ .chat-header {
1021
+ background: white;
1022
+ padding: 1rem;
1023
+ border-bottom: 1px solid var(--border);
1024
+ display: flex;
1025
+ justify-content: space-between;
1026
+ align-items: center;
1027
+ }
1028
+
1029
+ .provider-badge {
1030
+ display: flex;
1031
+ align-items: center;
1032
+ gap: 1rem;
1033
+ }
1034
+
1035
+ .avatar {
1036
+ width: 48px;
1037
+ height: 48px;
1038
+ background: var(--primary);
1039
+ color: white;
1040
+ border-radius: 50%;
1041
+ display: flex;
1042
+ align-items: center;
1043
+ justify-content: center;
1044
+ font-weight: bold;
1045
+ font-size: 1.2rem;
1046
+ }
1047
+
1048
+ .chat-messages {
1049
+ flex: 1;
1050
+ overflow-y: auto;
1051
+ padding: 2rem;
1052
+ background: #f8f9fa;
1053
+ }
1054
+
1055
+ .message {
1056
+ margin-bottom: 1.5rem;
1057
+ max-width: 80%;
1058
+ }
1059
+
1060
+ .user-message {
1061
+ margin-left: auto;
1062
+ }
1063
+
1064
+ .user-message .message-content {
1065
+ background: var(--primary);
1066
+ color: white;
1067
+ border-radius: 18px 18px 4px 18px;
1068
+ }
1069
+
1070
+ .ai-message .message-content {
1071
+ background: white;
1072
+ color: var(--dark);
1073
+ border-radius: 18px 18px 18px 4px;
1074
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
1075
+ }
1076
+
1077
+ .message-content {
1078
+ padding: 1rem;
1079
+ line-height: 1.5;
1080
+ }
1081
+
1082
+ .message.escalated .message-content {
1083
+ border-left: 4px solid var(--warning);
1084
+ background: #fff9e6;
1085
+ }
1086
+
1087
+ .escalation-badge {
1088
+ background: var(--warning);
1089
+ color: white;
1090
+ padding: 0.25rem 0.5rem;
1091
+ border-radius: 4px;
1092
+ font-size: 0.8rem;
1093
+ font-weight: bold;
1094
+ }
1095
+
1096
+ .message-input {
1097
+ padding: 1rem;
1098
+ background: white;
1099
+ border-top: 1px solid var(--border);
1100
+ display: flex;
1101
+ gap: 1rem;
1102
+ }
1103
+
1104
+ .message-input textarea {
1105
+ flex: 1;
1106
+ padding: 1rem;
1107
+ border: 2px solid var(--border);
1108
+ border-radius: 8px;
1109
+ resize: none;
1110
+ font-family: inherit;
1111
+ font-size: 1rem;
1112
+ }
1113
+
1114
+ .message-input textarea:focus {
1115
+ outline: none;
1116
+ border-color: var(--primary);
1117
+ }
1118
+
1119
+ .message-input button {
1120
+ background: var(--primary);
1121
+ color: white;
1122
+ border: none;
1123
+ padding: 0 2rem;
1124
+ border-radius: 8px;
1125
+ font-weight: 600;
1126
+ cursor: pointer;
1127
+ }
1128
+
1129
+ .safety-footer {
1130
+ background: #fff9e6;
1131
+ padding: 1rem;
1132
+ text-align: center;
1133
+ font-size: 0.9rem;
1134
+ border-top: 1px solid #ffd166;
1135
+ }
1136
+
1137
+ .urgent-warning {
1138
+ color: var(--danger);
1139
+ font-weight: bold;
1140
+ }
1141
+
1142
+ /* Responsive Design */
1143
+ @media (max-width: 768px) {
1144
+ .hero-container {
1145
+ grid-template-columns: 1fr;
1146
+ gap: 2rem;
1147
+ }
1148
+
1149
+ .nav-links {
1150
+ display: none;
1151
+ }
1152
+
1153
+ .message {
1154
+ max-width: 90%;
1155
+ }
1156
+ }
1157
+ ```
1158
+
1159
+ ## 🚀 **Deployment Checklist**
1160
+
1161
+ ### **Week 1: MVP Launch**
1162
+ - [ ] Create homepage with provider search
1163
+ - [ ] Implement basic chat interface
1164
+ - [ ] Add provider directory API
1165
+ - [ ] Deploy to staging environment
1166
+ - [ ] Test end-to-end flow
1167
+
1168
+ ### **Week 2: Enhancements**
1169
+ - [ ] Add provider profiles
1170
+ - [ ] Implement session management
1171
+ - [ ] Add safety info modals
1172
+ - [ ] Create provider signup form
1173
+ - [ ] Add analytics tracking
1174
+
1175
+ ### **Week 3: Polish & Launch**
1176
+ - [ ] Mobile responsiveness testing
1177
+ - [ ] Performance optimization
1178
+ - [ ] SEO meta tags
1179
+ - [ ] Privacy policy page
1180
+ - [ ] Launch to production
1181
+
1182
+ ## 📈 **Growth Strategy**
1183
+
1184
+ ### **Phase 1: Patient Acquisition**
1185
+ 1. **Provider referrals** - Existing providers share their EvalDoc link
1186
+ 2. **Clinic partnerships** - Embed widget on clinic websites
1187
+ 3. **Health directories** - List in healthcare directories
1188
+
1189
+ ### **Phase 2: Network Effects**
1190
+ 1. **Provider reviews** - Patients can rate their experience
1191
+ 2. **Specialty filtering** - Find specialists by condition
1192
+ 3. **Appointment booking** - Direct scheduling integration
1193
+
1194
+ ### **Phase 3: Advanced Features**
1195
+ 1. **Patient accounts** - Save conversation history
1196
+ 2. **File upload** - Share documents with provider
1197
+ 3. **Telehealth integration** - Video consult upsell
1198
+ 4. **Insurance verification** - Check coverage
1199
+
1200
+ ## 💡 **Key Advantages of This Approach**
1201
+
1202
+ 1. **Lower Barrier to Entry** - Patients don't need accounts
1203
+ 2. **Provider Control** - Doctors manage their own presence
1204
+ 3. **Scalable** - New providers can join easily
1205
+ 4. **Trust Building** - Professional interface builds confidence
1206
+ 5. **Data Ownership** - Providers own patient relationships
1207
+
1208
+ This transforms EvalDoc from a backend service into a **complete patient engagement platform**