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,513 @@
1
+ // DOM Elements
2
+ const providerName = document.getElementById("providerName");
3
+ const providerMeta = document.getElementById("providerMeta");
4
+ const welcomeMessage = document.getElementById("welcomeMessage");
5
+ const topicSelect = document.getElementById("topicSelect");
6
+ const messages = document.getElementById("messages");
7
+ const messageInput = document.getElementById("messageInput");
8
+ const sendBtn = document.getElementById("sendBtn");
9
+ const brandBlock = document.getElementById("brandBlock");
10
+ const brandLogo = document.getElementById("brandLogo");
11
+ const ratingPanel = document.getElementById("ratingPanel");
12
+
13
+ // Auth elements
14
+ const authModal = document.getElementById("authModal");
15
+ const googleSignInBtn = document.getElementById("googleSignInBtn");
16
+ const userInfo = document.getElementById("userInfo");
17
+ const userPhoto = document.getElementById("userPhoto");
18
+ const userName = document.getElementById("userName");
19
+
20
+ // Payment elements
21
+ const paymentModal = document.getElementById("paymentModal");
22
+ const paymentTitle = document.getElementById("paymentTitle");
23
+ const paymentDescription = document.getElementById("paymentDescription");
24
+ const paymentForm = document.getElementById("paymentForm");
25
+ const paymentProcessing = document.getElementById("paymentProcessing");
26
+ const paymentSuccess = document.getElementById("paymentSuccess");
27
+ const paymentSubmitBtn = document.getElementById("paymentSubmitBtn");
28
+ const paymentCancelBtn = document.getElementById("paymentCancelBtn");
29
+ const cardErrors = document.getElementById("card-errors");
30
+
31
+ let stripe = null;
32
+ let cardElement = null;
33
+ let currentPaymentConfig = null;
34
+
35
+ // State
36
+ let currentUser = null;
37
+ let currentProvider = null;
38
+ let currentThread = null;
39
+ let lastConversationId = null;
40
+
41
+ // Initialize Firebase
42
+ if (window.EVALDOC_FIREBASE_CONFIG && !firebase.apps.length) {
43
+ firebase.initializeApp(window.EVALDOC_FIREBASE_CONFIG);
44
+ }
45
+
46
+ // Utility functions
47
+ function getHandle() {
48
+ const path = window.location.pathname.replace(/^\//, "");
49
+ return path || "";
50
+ }
51
+
52
+ function getSessionId() {
53
+ const key = "evaldoc_session";
54
+ let sessionId = localStorage.getItem(key);
55
+ if (!sessionId) {
56
+ sessionId = `sess_${Math.random().toString(36).slice(2, 10)}`;
57
+ localStorage.setItem(key, sessionId);
58
+ }
59
+ return sessionId;
60
+ }
61
+
62
+ function addMessage(role, text, timestamp = null) {
63
+ const bubble = document.createElement("div");
64
+ bubble.className = `message ${role}`;
65
+ bubble.textContent = text;
66
+
67
+ if (timestamp) {
68
+ const time = document.createElement("div");
69
+ time.className = "message-time";
70
+ time.textContent = formatTime(timestamp);
71
+ bubble.appendChild(time);
72
+ }
73
+
74
+ messages.appendChild(bubble);
75
+ messages.scrollTop = messages.scrollHeight;
76
+ }
77
+
78
+ function formatTime(timestamp) {
79
+ if (!timestamp) return '';
80
+ const date = timestamp.toDate ? timestamp.toDate() : new Date(timestamp._seconds * 1000);
81
+ return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
82
+ }
83
+
84
+ function setBranding(provider) {
85
+ if (provider.displayName) providerName.textContent = provider.displayName;
86
+ if (provider.specialty || provider.country) {
87
+ providerMeta.textContent = [provider.specialty, provider.country].filter(Boolean).join(" · ");
88
+ }
89
+ if (provider.welcomeMessage) welcomeMessage.textContent = provider.welcomeMessage;
90
+
91
+ if (provider.brandPrimaryColor && provider.brandSecondaryColor) {
92
+ brandBlock.style.background = `linear-gradient(135deg, ${provider.brandPrimaryColor}, ${provider.brandSecondaryColor})`;
93
+ }
94
+ const initials = (provider.displayName || "ED")
95
+ .split(" ")
96
+ .map((part) => part[0])
97
+ .join("")
98
+ .slice(0, 2)
99
+ .toUpperCase();
100
+ brandLogo.textContent = initials || "ED";
101
+ }
102
+
103
+ // Load provider info
104
+ async function loadProvider() {
105
+ const handle = getHandle();
106
+ if (!handle) {
107
+ providerName.textContent = "Invalid link";
108
+ providerMeta.textContent = "Missing provider handle";
109
+ return;
110
+ }
111
+
112
+ const res = await fetch(`/api/provider?handle=${encodeURIComponent(handle)}`);
113
+ if (!res.ok) {
114
+ providerName.textContent = "Provider unavailable";
115
+ providerMeta.textContent = "Please check the link";
116
+ return;
117
+ }
118
+
119
+ const provider = await res.json();
120
+ currentProvider = provider;
121
+ setBranding(provider);
122
+
123
+ const topics = provider.enabledTopics || [];
124
+ topicSelect.innerHTML = "";
125
+ topics.forEach((topic) => {
126
+ const option = document.createElement("option");
127
+ option.value = topic;
128
+ option.textContent = topic.replace(/_/g, " ");
129
+ topicSelect.appendChild(option);
130
+ });
131
+
132
+ if (!topics.length) {
133
+ const option = document.createElement("option");
134
+ option.value = "";
135
+ option.textContent = "No topics available";
136
+ topicSelect.appendChild(option);
137
+ }
138
+ }
139
+
140
+ // Authentication
141
+ function showAuthModal() {
142
+ authModal.style.display = "flex";
143
+ }
144
+
145
+ function hideAuthModal() {
146
+ authModal.style.display = "none";
147
+ }
148
+
149
+ async function signInWithGoogle() {
150
+ try {
151
+ const provider = new firebase.auth.GoogleAuthProvider();
152
+ const result = await firebase.auth().signInWithPopup(provider);
153
+ console.log('Signed in:', result.user.email);
154
+ hideAuthModal();
155
+ } catch (error) {
156
+ console.error('Sign in error:', error);
157
+ alert('Failed to sign in. Please try again.');
158
+ }
159
+ }
160
+
161
+ googleSignInBtn.addEventListener('click', signInWithGoogle);
162
+
163
+ // Handle auth state changes
164
+ firebase.auth().onAuthStateChanged(async (user) => {
165
+ if (user) {
166
+ currentUser = user;
167
+
168
+ // Update UI to show user info
169
+ userName.textContent = user.displayName || user.email;
170
+ if (user.photoURL) {
171
+ userPhoto.src = user.photoURL;
172
+ }
173
+ userInfo.style.display = 'flex';
174
+
175
+ // Create or get patient profile
176
+ await ensurePatientProfile(user);
177
+
178
+ // Load or create thread
179
+ await loadThread();
180
+ } else {
181
+ currentUser = null;
182
+ userInfo.style.display = 'none';
183
+ }
184
+ });
185
+
186
+ // Create patient profile if doesn't exist
187
+ async function ensurePatientProfile(user) {
188
+ try {
189
+ const token = await user.getIdToken();
190
+ const res = await fetch('/api/patient/me', {
191
+ headers: {
192
+ 'Authorization': `Bearer ${token}`
193
+ }
194
+ });
195
+
196
+ if (res.status === 404) {
197
+ // Create patient profile
198
+ await fetch('/api/patient/create', {
199
+ method: 'POST',
200
+ headers: {
201
+ 'Authorization': `Bearer ${token}`,
202
+ 'Content-Type': 'application/json'
203
+ },
204
+ body: JSON.stringify({
205
+ email: user.email,
206
+ displayName: user.displayName,
207
+ photoURL: user.photoURL
208
+ })
209
+ });
210
+ }
211
+ } catch (error) {
212
+ console.error('Error ensuring patient profile:', error);
213
+ }
214
+ }
215
+
216
+ // Load or create thread with doctor
217
+ async function loadThread() {
218
+ if (!currentUser || !currentProvider) return;
219
+
220
+ try {
221
+ const token = await currentUser.getIdToken();
222
+ const handle = getHandle();
223
+
224
+ // Get or create thread
225
+ const res = await fetch('/api/patient/thread', {
226
+ method: 'POST',
227
+ headers: {
228
+ 'Authorization': `Bearer ${token}`,
229
+ 'Content-Type': 'application/json'
230
+ },
231
+ body: JSON.stringify({
232
+ doctorHandle: handle
233
+ })
234
+ });
235
+
236
+ if (!res.ok) {
237
+ console.error('Failed to load thread');
238
+ return;
239
+ }
240
+
241
+ const data = await res.json();
242
+ currentThread = data.thread;
243
+
244
+ // Load existing messages
245
+ await loadMessages();
246
+ } catch (error) {
247
+ console.error('Error loading thread:', error);
248
+ }
249
+ }
250
+
251
+ // Load messages from thread
252
+ async function loadMessages() {
253
+ if (!currentThread || !currentUser) return;
254
+
255
+ try {
256
+ const token = await currentUser.getIdToken();
257
+ const res = await fetch(`/api/patient/thread/${currentThread.id}/messages`, {
258
+ headers: {
259
+ 'Authorization': `Bearer ${token}`
260
+ }
261
+ });
262
+
263
+ if (!res.ok) return;
264
+
265
+ const data = await res.json();
266
+ messages.innerHTML = ''; // Clear existing messages
267
+
268
+ data.messages.forEach(msg => {
269
+ addMessage(msg.role, msg.message, msg.createdAt);
270
+ });
271
+ } catch (error) {
272
+ console.error('Error loading messages:', error);
273
+ }
274
+ }
275
+
276
+ // Send message
277
+ async function sendMessage() {
278
+ const text = messageInput.value.trim();
279
+ if (!text) return;
280
+
281
+ // Check if user is signed in
282
+ if (!currentUser) {
283
+ showAuthModal();
284
+ return;
285
+ }
286
+
287
+ if (!currentThread) {
288
+ addMessage("assistant", "Setting up conversation...");
289
+ await loadThread();
290
+ if (!currentThread) {
291
+ addMessage("assistant", "Unable to start conversation. Please refresh.");
292
+ return;
293
+ }
294
+ }
295
+
296
+ const topic = topicSelect.value;
297
+
298
+ addMessage("user", text);
299
+ messageInput.value = "";
300
+ sendBtn.disabled = true;
301
+ ratingPanel.classList.add("hidden");
302
+
303
+ try {
304
+ const token = await currentUser.getIdToken();
305
+ const res = await fetch("/api/patient/message", {
306
+ method: "POST",
307
+ headers: {
308
+ "Content-Type": "application/json",
309
+ "Authorization": `Bearer ${token}`
310
+ },
311
+ body: JSON.stringify({
312
+ threadId: currentThread.id,
313
+ message: text,
314
+ topic: topic
315
+ })
316
+ });
317
+
318
+ const data = await res.json();
319
+
320
+ // Handle payment required (402 response)
321
+ if (res.status === 402 && data.paymentRequired) {
322
+ await handlePaymentRequired(data.paymentConfig);
323
+ return;
324
+ }
325
+
326
+ if (!res.ok) {
327
+ addMessage("assistant", data.error || "Unable to respond right now.");
328
+ return;
329
+ }
330
+
331
+ addMessage("assistant", data.response);
332
+
333
+ if (data.escalation) {
334
+ addMessage("assistant", "⚠️ This requires attention from a licensed clinician. The doctor will be notified.");
335
+ }
336
+ } catch (err) {
337
+ console.error('Send message error:', err);
338
+ addMessage("assistant", "Network error. Please try again.");
339
+ } finally {
340
+ sendBtn.disabled = false;
341
+ }
342
+ }
343
+
344
+ sendBtn.addEventListener("click", sendMessage);
345
+ messageInput.addEventListener("keydown", (event) => {
346
+ if (event.key === "Enter") {
347
+ sendMessage();
348
+ }
349
+ });
350
+
351
+ // Payment handling
352
+ async function handlePaymentRequired(config) {
353
+ currentPaymentConfig = config;
354
+
355
+ if (config.pricingModel === 'subscription') {
356
+ // Redirect to Stripe Checkout for subscription
357
+ paymentTitle.textContent = "Subscription Required";
358
+ paymentDescription.textContent =
359
+ `This provider requires a subscription ($${config.subscriptionPrice}/month). You'll be redirected to complete payment.`;
360
+ paymentForm.innerHTML =
361
+ '<button id="subscribeBtn" class="payment-submit-btn">Subscribe Now</button>' +
362
+ '<button id="paymentCancelBtnSub" class="payment-cancel-btn">Cancel</button>';
363
+ paymentModal.style.display = "flex";
364
+
365
+ document.getElementById("subscribeBtn").addEventListener("click", async () => {
366
+ try {
367
+ document.getElementById("subscribeBtn").disabled = true;
368
+ document.getElementById("subscribeBtn").textContent = "Redirecting...";
369
+ const token = await currentUser.getIdToken();
370
+ const res = await fetch("/api/payment/create-checkout-session", {
371
+ method: "POST",
372
+ headers: {
373
+ "Content-Type": "application/json",
374
+ "Authorization": `Bearer ${token}`
375
+ },
376
+ body: JSON.stringify({
377
+ handle: config.handle,
378
+ sessionId: currentUser.uid,
379
+ interval: "month"
380
+ })
381
+ });
382
+ const data = await res.json();
383
+ if (data.url) {
384
+ window.location.href = data.url;
385
+ } else {
386
+ addMessage("assistant", "Unable to start payment. Please try again.");
387
+ paymentModal.style.display = "none";
388
+ }
389
+ } catch (err) {
390
+ console.error("Checkout error:", err);
391
+ addMessage("assistant", "Payment error. Please try again.");
392
+ paymentModal.style.display = "none";
393
+ }
394
+ });
395
+
396
+ document.getElementById("paymentCancelBtnSub").addEventListener("click", () => {
397
+ paymentModal.style.display = "none";
398
+ });
399
+ } else if (config.pricingModel === 'per-chat') {
400
+ // Per-chat: create payment intent and show card form
401
+ paymentTitle.textContent = "Payment Required";
402
+ paymentDescription.textContent =
403
+ `This conversation costs $${config.perChatPrice}. Enter your card details below.`;
404
+
405
+ try {
406
+ const token = await currentUser.getIdToken();
407
+ const intentRes = await fetch("/api/payment/create-intent", {
408
+ method: "POST",
409
+ headers: {
410
+ "Content-Type": "application/json",
411
+ "Authorization": `Bearer ${token}`
412
+ },
413
+ body: JSON.stringify({
414
+ handle: config.handle,
415
+ sessionId: currentUser.uid
416
+ })
417
+ });
418
+ const intentData = await intentRes.json();
419
+
420
+ if (!intentData.clientSecret) {
421
+ addMessage("assistant", "Unable to set up payment. Please try again.");
422
+ return;
423
+ }
424
+
425
+ currentPaymentConfig.clientSecret = intentData.clientSecret;
426
+
427
+ // Initialize Stripe card element
428
+ if (!stripe && config.stripePublishableKey) {
429
+ stripe = Stripe(config.stripePublishableKey);
430
+ }
431
+ if (stripe && !cardElement) {
432
+ const elements = stripe.elements();
433
+ cardElement = elements.create("card");
434
+ cardElement.mount("#card-element");
435
+ cardElement.on("change", (event) => {
436
+ cardErrors.textContent = event.error ? event.error.message : "";
437
+ });
438
+ }
439
+
440
+ // Reset form state
441
+ paymentForm.innerHTML =
442
+ '<div id="card-element"></div>' +
443
+ '<div id="card-errors" role="alert"></div>' +
444
+ '<button id="paymentSubmitBtnPc" class="payment-submit-btn">Pay Now</button>' +
445
+ '<button id="paymentCancelBtnPc" class="payment-cancel-btn">Cancel</button>';
446
+
447
+ if (stripe) {
448
+ const elements = stripe.elements();
449
+ cardElement = elements.create("card");
450
+ cardElement.mount("#card-element");
451
+ cardElement.on("change", (event) => {
452
+ const errDiv = document.getElementById("card-errors");
453
+ if (errDiv) errDiv.textContent = event.error ? event.error.message : "";
454
+ });
455
+ }
456
+
457
+ document.getElementById("paymentSubmitBtnPc").addEventListener("click", async () => {
458
+ if (!currentPaymentConfig.clientSecret || !stripe || !cardElement) return;
459
+
460
+ document.getElementById("paymentSubmitBtnPc").disabled = true;
461
+ paymentProcessing.style.display = "block";
462
+
463
+ try {
464
+ const result = await stripe.confirmCardPayment(currentPaymentConfig.clientSecret, {
465
+ payment_method: {
466
+ card: cardElement,
467
+ billing_details: {
468
+ email: currentUser.email,
469
+ name: currentUser.displayName
470
+ }
471
+ }
472
+ });
473
+
474
+ if (result.error) {
475
+ throw new Error(result.error.message);
476
+ }
477
+
478
+ paymentProcessing.style.display = "none";
479
+ paymentSuccess.style.display = "block";
480
+
481
+ setTimeout(() => {
482
+ paymentModal.style.display = "none";
483
+ paymentSuccess.style.display = "none";
484
+ addMessage("assistant", "Payment successful! You can now continue chatting.");
485
+ loadThread();
486
+ }, 2000);
487
+ } catch (error) {
488
+ console.error("Payment error:", error);
489
+ paymentProcessing.style.display = "none";
490
+ document.getElementById("paymentSubmitBtnPc").disabled = false;
491
+ const errDiv = document.getElementById("card-errors");
492
+ if (errDiv) errDiv.textContent = error.message;
493
+ }
494
+ });
495
+
496
+ document.getElementById("paymentCancelBtnPc").addEventListener("click", () => {
497
+ paymentModal.style.display = "none";
498
+ });
499
+
500
+ paymentModal.style.display = "flex";
501
+ } catch (err) {
502
+ console.error("Payment setup error:", err);
503
+ addMessage("assistant", "Payment setup error. Please try again.");
504
+ }
505
+ }
506
+ }
507
+
508
+ paymentCancelBtn.addEventListener("click", () => {
509
+ paymentModal.style.display = "none";
510
+ });
511
+
512
+ // Initialize
513
+ loadProvider();