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.
- package/.env.staging.example +19 -0
- package/.firebaserc +6 -0
- package/.github/workflows/auto-merge-to-main.yml +28 -0
- package/.github/workflows/ci.yml +27 -0
- package/.github/workflows/npxhub-publish.yml +170 -0
- package/.prettierignore +4 -0
- package/.prettierrc.json +7 -0
- package/README.md +142 -0
- package/SRS.md +448 -0
- package/apphosting.staging.yaml +48 -0
- package/apphosting.yaml +50 -0
- package/bin/evaldoc.cjs +105 -0
- package/cors.json +8 -0
- package/docs/ARCHITECTURE.md +769 -0
- package/docs/ARCHITECTURE_SUMMARY.md +323 -0
- package/docs/DEPLOYMENT.md +86 -0
- package/docs/DEPLOYMENT_STEPS.md +373 -0
- package/docs/DEPLOY_FIRESTORE_RULES.md +239 -0
- package/docs/DEPLOY_STORAGE_RULES.md +82 -0
- package/docs/DOCTOR_TUTORIAL.md +467 -0
- package/docs/FIREBASE_STORAGE_CORS_FIX.md +131 -0
- package/docs/FIXES_APPLIED.md +128 -0
- package/docs/IMPLEMENTATION.md +498 -0
- package/docs/IMPLEMENTATION_COMPLETE.md +317 -0
- package/docs/LLM-SETUP.md +216 -0
- package/docs/MANUAL_TEST_GUIDE.md +327 -0
- package/docs/PATIENT_GUIDE.md +451 -0
- package/docs/PLATFORM_OVERVIEW.md +194 -0
- package/docs/QUICKSTART.md +252 -0
- package/docs/QUICK_REFERENCE.md +222 -0
- package/docs/READY_TO_TEST.md +383 -0
- package/docs/RUN.md +237 -0
- package/docs/START_TESTING.md +130 -0
- package/docs/STRIPE_CONNECT_SETUP.md +454 -0
- package/docs/SYSTEM_READY.md +345 -0
- package/docs/THREADS_IMPLEMENTATION_COMPLETE.md +431 -0
- package/docs/THREAD_IMPLEMENTATION_GUIDE.md +345 -0
- package/docs/firebasefiles.md +21 -0
- package/docs/homepage.md +1208 -0
- package/docs/payments.md +159 -0
- package/eslint.config.js +76 -0
- package/firebase.json +27 -0
- package/firestore.indexes.json +88 -0
- package/firestore.rules +125 -0
- package/functions/package-lock.json +2884 -0
- package/functions/package.json +20 -0
- package/functions/src/analytics.js +101 -0
- package/functions/src/audit.js +28 -0
- package/functions/src/auth.js +19 -0
- package/functions/src/drafts.js +128 -0
- package/functions/src/feedback.js +164 -0
- package/functions/src/firebase-init.js +29 -0
- package/functions/src/i18n.js +134 -0
- package/functions/src/limits.js +27 -0
- package/functions/src/llm.js +271 -0
- package/functions/src/ner.js +162 -0
- package/functions/src/notifications.js +273 -0
- package/functions/src/orgs.js +127 -0
- package/functions/src/patient-api.js +545 -0
- package/functions/src/payment.js +830 -0
- package/functions/src/safety.js +188 -0
- package/functions/src/stripe-config.js +33 -0
- package/functions/src/topics.js +94 -0
- package/functions/src/triage.js +191 -0
- package/functions/src/utils.js +28 -0
- package/functions/src/validate.js +177 -0
- package/functions/test-firebase.js +36 -0
- package/jest.config.js +18 -0
- package/package.json +52 -0
- package/public/404.html +93 -0
- package/public/app/analytics.html +336 -0
- package/public/app/analytics.js +177 -0
- package/public/app/app.css +1400 -0
- package/public/app/app.js +1754 -0
- package/public/app/config.js +14 -0
- package/public/app/inbox-threads.js +843 -0
- package/public/app/inbox.html +598 -0
- package/public/app/inbox.js +397 -0
- package/public/app/index.html +612 -0
- package/public/assets/logo.jpg +0 -0
- package/public/chat.html +137 -0
- package/public/css/chat.css +870 -0
- package/public/css/landing-enhance.css +298 -0
- package/public/css/styles.css +1198 -0
- package/public/favicon.svg +5 -0
- package/public/find-doctor.html +220 -0
- package/public/index.html +399 -0
- package/public/js/chat-threads.js +513 -0
- package/public/js/chat.js +832 -0
- package/public/js/search.js +41 -0
- package/public/privacy.html +101 -0
- package/public/robots.txt +6 -0
- package/public/site.webmanifest +12 -0
- package/public/terms.html +110 -0
- package/scripts/create-test-provider.js +55 -0
- package/scripts/deploy-staging.sh +51 -0
- package/scripts/fix-provider.js +39 -0
- package/scripts/quick-test.sh +37 -0
- package/scripts/test-chat.sh +54 -0
- package/scripts/test-multi-turn.js +137 -0
- package/scripts/test-ner-simple.js +28 -0
- package/scripts/test-ner.js +60 -0
- package/scripts/test-stripe-connect.js +304 -0
- package/scripts/test-webhook-secret.sh +24 -0
- package/server.js +3864 -0
- package/storage.rules +22 -0
- package/test-api.json +1 -0
- package/tests/emulator/README.md +38 -0
- package/tests/emulator/counters.emulator.test.js +81 -0
- package/tests/integration/api-account-selfserve.test.js +312 -0
- package/tests/integration/api-admin-digest.test.js +202 -0
- package/tests/integration/api-chat-satisfaction.test.js +259 -0
- package/tests/integration/api-doctor-drafts.test.js +236 -0
- package/tests/integration/api-doctor-inbox.test.js +401 -0
- package/tests/integration/api-org.test.js +328 -0
- package/tests/integration/api-payment.test.js +664 -0
- package/tests/integration/api-provider-settings.test.js +475 -0
- package/tests/integration/api-qr.test.js +121 -0
- package/tests/integration/api-security.test.js +241 -0
- package/tests/integration/api-stripe-search.test.js +303 -0
- package/tests/integration/api-webhook-deletion.test.js +231 -0
- package/tests/mocks/firebase-admin.js +220 -0
- package/tests/unit/analytics.test.js +151 -0
- package/tests/unit/audit-bugs.test.js +203 -0
- package/tests/unit/chat-ui-logic.test.js +283 -0
- package/tests/unit/drafts.test.js +104 -0
- package/tests/unit/feedback.test.js +148 -0
- package/tests/unit/i18n.test.js +229 -0
- package/tests/unit/llm-retry.test.js +122 -0
- package/tests/unit/notifications.test.js +155 -0
- package/tests/unit/orgs.test.js +328 -0
- package/tests/unit/payment.test.js +889 -0
- package/tests/unit/safety-edge-cases.test.js +509 -0
- package/tests/unit/safety-srs-compliance.test.js +252 -0
- package/tests/unit/safety.test.js +291 -0
- package/tests/unit/server-helpers.test.js +774 -0
- package/tests/unit/topics-limits.test.js +158 -0
- package/tests/unit/triage.test.js +152 -0
- 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();
|