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,397 @@
|
|
|
1
|
+
const authRequired = document.getElementById("authRequired");
|
|
2
|
+
const inboxContainer = document.getElementById("inboxContainer");
|
|
3
|
+
const conversationsList = document.getElementById("conversationsList");
|
|
4
|
+
const conversationDetail = document.getElementById("conversationDetail");
|
|
5
|
+
const replySection = document.getElementById("replySection");
|
|
6
|
+
const replyInput = document.getElementById("replyInput");
|
|
7
|
+
const sendReplyBtn = document.getElementById("sendReplyBtn");
|
|
8
|
+
const signOutBtn = document.getElementById("signOutBtn");
|
|
9
|
+
|
|
10
|
+
let currentProvider = null;
|
|
11
|
+
let conversations = [];
|
|
12
|
+
let selectedConversation = null;
|
|
13
|
+
let currentFilter = "all";
|
|
14
|
+
|
|
15
|
+
async function fetchWithAuth(path, options = {}) {
|
|
16
|
+
const user = firebase.auth().currentUser;
|
|
17
|
+
if (!user) throw new Error("Not authenticated");
|
|
18
|
+
|
|
19
|
+
const token = await user.getIdToken();
|
|
20
|
+
const headers = Object.assign({}, options.headers || {}, {
|
|
21
|
+
Authorization: `Bearer ${token}`,
|
|
22
|
+
});
|
|
23
|
+
return fetch(path, { ...options, headers });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function loadProvider() {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetchWithAuth("/api/me");
|
|
29
|
+
if (res.status === 404) return null;
|
|
30
|
+
if (!res.ok) throw new Error("Unable to load provider");
|
|
31
|
+
const provider = await res.json();
|
|
32
|
+
return provider;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error("Error loading provider:", err);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function loadConversations() {
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetchWithAuth("/api/conversations?limit=100");
|
|
42
|
+
if (!res.ok) throw new Error("Unable to load conversations");
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
conversations = data.conversations || [];
|
|
45
|
+
renderConversationsList();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error("Error loading conversations:", err);
|
|
48
|
+
conversationsList.innerHTML =
|
|
49
|
+
'<p style="color: #6b7280;">Unable to load conversations</p>';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatTime(timestamp) {
|
|
54
|
+
if (!timestamp) return "Unknown";
|
|
55
|
+
const date = timestamp.toDate
|
|
56
|
+
? timestamp.toDate()
|
|
57
|
+
: new Date(timestamp._seconds * 1000);
|
|
58
|
+
const now = new Date();
|
|
59
|
+
const diff = now - date;
|
|
60
|
+
|
|
61
|
+
const minutes = Math.floor(diff / 60000);
|
|
62
|
+
const hours = Math.floor(diff / 3600000);
|
|
63
|
+
const days = Math.floor(diff / 86400000);
|
|
64
|
+
|
|
65
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
66
|
+
if (hours < 24) return `${hours}h ago`;
|
|
67
|
+
if (days < 7) return `${days}d ago`;
|
|
68
|
+
|
|
69
|
+
return date.toLocaleDateString();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getClientIdentifier(conversation) {
|
|
73
|
+
if (conversation.clientName) return conversation.clientName;
|
|
74
|
+
if (conversation.clientEmail) return conversation.clientEmail;
|
|
75
|
+
if (conversation.clientPhone) return conversation.clientPhone;
|
|
76
|
+
if (conversation.sessionId) return `Session ${conversation.sessionId.substring(0, 8)}`;
|
|
77
|
+
return "Anonymous Patient";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getMessagePreview(conversation) {
|
|
81
|
+
const lastMessage = conversation.lastUserMessage || conversation.lastAssistantMessage || "";
|
|
82
|
+
if (lastMessage) {
|
|
83
|
+
return (
|
|
84
|
+
lastMessage.substring(0, 60) +
|
|
85
|
+
(lastMessage.length > 60 ? "..." : "")
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return "No messages yet";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function filterConversations() {
|
|
92
|
+
const now = new Date();
|
|
93
|
+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
94
|
+
const weekStart = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
95
|
+
|
|
96
|
+
return conversations.filter((conv) => {
|
|
97
|
+
if (currentFilter === "all") return true;
|
|
98
|
+
if (currentFilter === "escalated") return conv.isEscalated === true;
|
|
99
|
+
if (currentFilter === "today") {
|
|
100
|
+
const convDate = conv.lastMessageAt?.toDate
|
|
101
|
+
? conv.lastMessageAt.toDate()
|
|
102
|
+
: new Date(conv.lastMessageAt?._seconds * 1000);
|
|
103
|
+
return convDate >= todayStart;
|
|
104
|
+
}
|
|
105
|
+
if (currentFilter === "week") {
|
|
106
|
+
const convDate = conv.lastMessageAt?.toDate
|
|
107
|
+
? conv.lastMessageAt.toDate()
|
|
108
|
+
: new Date(conv.lastMessageAt?._seconds * 1000);
|
|
109
|
+
return convDate >= weekStart;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderConversationsList() {
|
|
116
|
+
const filtered = filterConversations();
|
|
117
|
+
|
|
118
|
+
if (filtered.length === 0) {
|
|
119
|
+
conversationsList.innerHTML =
|
|
120
|
+
'<p style="color: rgba(255,255,255,0.5); text-align: center; margin-top: 2rem;">No conversations found</p>';
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
conversationsList.innerHTML = "";
|
|
125
|
+
filtered.forEach((conv) => {
|
|
126
|
+
const item = document.createElement("div");
|
|
127
|
+
item.className = "conversation-item";
|
|
128
|
+
if (selectedConversation && selectedConversation.id === conv.id) {
|
|
129
|
+
item.classList.add("active");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const clientId = getClientIdentifier(conv);
|
|
133
|
+
const preview = getMessagePreview(conv);
|
|
134
|
+
const time = formatTime(
|
|
135
|
+
conv.lastMessageAt || conv.updatedAt || conv.createdAt
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const header = document.createElement("div");
|
|
139
|
+
header.className = "conversation-header";
|
|
140
|
+
const clientDiv = document.createElement("div");
|
|
141
|
+
clientDiv.className = "conversation-client";
|
|
142
|
+
clientDiv.textContent = clientId;
|
|
143
|
+
const timeDiv = document.createElement("div");
|
|
144
|
+
timeDiv.className = "conversation-time";
|
|
145
|
+
timeDiv.textContent = time;
|
|
146
|
+
header.appendChild(clientDiv);
|
|
147
|
+
header.appendChild(timeDiv);
|
|
148
|
+
item.appendChild(header);
|
|
149
|
+
|
|
150
|
+
const topicDiv = document.createElement("div");
|
|
151
|
+
topicDiv.className = "conversation-topic";
|
|
152
|
+
topicDiv.textContent = (conv.currentTopic || "general_health_info").replace(/_/g, " ");
|
|
153
|
+
item.appendChild(topicDiv);
|
|
154
|
+
|
|
155
|
+
const previewDiv = document.createElement("div");
|
|
156
|
+
previewDiv.className = "conversation-preview";
|
|
157
|
+
previewDiv.textContent = preview;
|
|
158
|
+
item.appendChild(previewDiv);
|
|
159
|
+
|
|
160
|
+
if (conv.isEscalated) {
|
|
161
|
+
const badge = document.createElement("span");
|
|
162
|
+
badge.className = "conversation-badge escalated";
|
|
163
|
+
badge.textContent = "⚠️ Escalated";
|
|
164
|
+
item.appendChild(badge);
|
|
165
|
+
}
|
|
166
|
+
if (conv.unreadByDoctor) {
|
|
167
|
+
const badge = document.createElement("span");
|
|
168
|
+
badge.className = "conversation-badge";
|
|
169
|
+
badge.style.cssText = "background: var(--primary); color: white;";
|
|
170
|
+
badge.textContent = "New";
|
|
171
|
+
item.appendChild(badge);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
item.addEventListener("click", () => {
|
|
175
|
+
selectedConversation = conv;
|
|
176
|
+
renderConversationsList();
|
|
177
|
+
renderConversationDetail(conv);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
conversationsList.appendChild(item);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function renderConversationDetail(conversation) {
|
|
185
|
+
conversationDetail.innerHTML =
|
|
186
|
+
'<div style="text-align: center; padding: 2rem;">Loading...</div>';
|
|
187
|
+
replySection.classList.add("hidden");
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
// Fetch full conversation details with messages
|
|
191
|
+
const res = await fetchWithAuth(`/api/conversation/${conversation.id}`);
|
|
192
|
+
if (!res.ok) throw new Error("Unable to load conversation details");
|
|
193
|
+
const fullConv = await res.json();
|
|
194
|
+
const messages = fullConv.messages || [];
|
|
195
|
+
|
|
196
|
+
const clientId = getClientIdentifier(fullConv);
|
|
197
|
+
|
|
198
|
+
conversationDetail.innerHTML = "";
|
|
199
|
+
|
|
200
|
+
const detailHeader = document.createElement("div");
|
|
201
|
+
detailHeader.className = "detail-header";
|
|
202
|
+
|
|
203
|
+
const titleDiv = document.createElement("div");
|
|
204
|
+
titleDiv.className = "detail-title";
|
|
205
|
+
titleDiv.textContent = clientId;
|
|
206
|
+
detailHeader.appendChild(titleDiv);
|
|
207
|
+
|
|
208
|
+
const metaDiv = document.createElement("div");
|
|
209
|
+
metaDiv.className = "detail-meta";
|
|
210
|
+
const msgCountItem = document.createElement("div");
|
|
211
|
+
msgCountItem.className = "detail-meta-item";
|
|
212
|
+
const msgCountLabel = document.createElement("span");
|
|
213
|
+
msgCountLabel.className = "detail-meta-label";
|
|
214
|
+
msgCountLabel.textContent = `${messages.length} msgs`;
|
|
215
|
+
msgCountItem.appendChild(msgCountLabel);
|
|
216
|
+
metaDiv.appendChild(msgCountItem);
|
|
217
|
+
|
|
218
|
+
const timeItem = document.createElement("div");
|
|
219
|
+
timeItem.className = "detail-meta-item";
|
|
220
|
+
const timeDot = document.createElement("span");
|
|
221
|
+
timeDot.className = "detail-meta-label";
|
|
222
|
+
timeDot.textContent = "•";
|
|
223
|
+
const timeValue = document.createElement("span");
|
|
224
|
+
timeValue.className = "detail-meta-value";
|
|
225
|
+
timeValue.textContent = formatTime(fullConv.lastMessageAt || fullConv.createdAt);
|
|
226
|
+
timeItem.appendChild(timeDot);
|
|
227
|
+
timeItem.appendChild(timeValue);
|
|
228
|
+
metaDiv.appendChild(timeItem);
|
|
229
|
+
|
|
230
|
+
if (fullConv.isEscalated) {
|
|
231
|
+
const escItem = document.createElement("div");
|
|
232
|
+
escItem.className = "detail-meta-item";
|
|
233
|
+
const escDot = document.createElement("span");
|
|
234
|
+
escDot.className = "detail-meta-label";
|
|
235
|
+
escDot.textContent = "•";
|
|
236
|
+
const escVal = document.createElement("span");
|
|
237
|
+
escVal.className = "detail-meta-value";
|
|
238
|
+
escVal.style.color = "#ff6b6b";
|
|
239
|
+
escVal.textContent = "⚠️ Escalated";
|
|
240
|
+
escItem.appendChild(escDot);
|
|
241
|
+
escItem.appendChild(escVal);
|
|
242
|
+
metaDiv.appendChild(escItem);
|
|
243
|
+
}
|
|
244
|
+
detailHeader.appendChild(metaDiv);
|
|
245
|
+
|
|
246
|
+
if (fullConv.clientEmail || fullConv.clientPhone || fullConv.clientName) {
|
|
247
|
+
const contactBox = document.createElement("div");
|
|
248
|
+
contactBox.style.cssText = "margin-top: 0.75rem; padding: 0.75rem; background: #e6f7f4; border-radius: 6px; border-left: 3px solid #297B56; font-size: 0.85rem; color: #111827;";
|
|
249
|
+
if (fullConv.clientName) {
|
|
250
|
+
const nameDiv = document.createElement("div");
|
|
251
|
+
nameDiv.style.cssText = "margin-bottom: 0.25rem; font-weight: 600;";
|
|
252
|
+
nameDiv.textContent = fullConv.clientName;
|
|
253
|
+
contactBox.appendChild(nameDiv);
|
|
254
|
+
}
|
|
255
|
+
if (fullConv.clientEmail) {
|
|
256
|
+
const emailDiv = document.createElement("div");
|
|
257
|
+
emailDiv.style.marginBottom = "0.25rem";
|
|
258
|
+
const emailLink = document.createElement("a");
|
|
259
|
+
emailLink.href = `mailto:${fullConv.clientEmail}`;
|
|
260
|
+
emailLink.style.cssText = "color: #297B56; text-decoration: underline;";
|
|
261
|
+
emailLink.textContent = fullConv.clientEmail;
|
|
262
|
+
emailDiv.appendChild(emailLink);
|
|
263
|
+
contactBox.appendChild(emailDiv);
|
|
264
|
+
}
|
|
265
|
+
if (fullConv.clientPhone) {
|
|
266
|
+
const phoneDiv = document.createElement("div");
|
|
267
|
+
const phoneLink = document.createElement("a");
|
|
268
|
+
phoneLink.href = `tel:${fullConv.clientPhone}`;
|
|
269
|
+
phoneLink.style.cssText = "color: #297B56; text-decoration: underline;";
|
|
270
|
+
phoneLink.textContent = fullConv.clientPhone;
|
|
271
|
+
phoneDiv.appendChild(phoneLink);
|
|
272
|
+
contactBox.appendChild(phoneDiv);
|
|
273
|
+
}
|
|
274
|
+
detailHeader.appendChild(contactBox);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
conversationDetail.appendChild(detailHeader);
|
|
278
|
+
|
|
279
|
+
const messagesList = document.createElement("div");
|
|
280
|
+
messagesList.className = "messages-list";
|
|
281
|
+
|
|
282
|
+
if (messages.length === 0) {
|
|
283
|
+
const emptyMsg = document.createElement("p");
|
|
284
|
+
emptyMsg.style.cssText = "text-align: center; color: #6b7280;";
|
|
285
|
+
emptyMsg.textContent = "No messages in this conversation";
|
|
286
|
+
messagesList.appendChild(emptyMsg);
|
|
287
|
+
} else {
|
|
288
|
+
messages.forEach((msg) => {
|
|
289
|
+
const isUser = msg.role === "patient" || msg.role === "user";
|
|
290
|
+
const roleClass = isUser ? "user" : msg.isDoctorManual ? "doctor" : "assistant";
|
|
291
|
+
const label = isUser ? "👤 Patient" : msg.isDoctorManual ? "👨⚕️ You" : "🤖 AI Assistant";
|
|
292
|
+
|
|
293
|
+
const block = document.createElement("div");
|
|
294
|
+
block.className = "message-block";
|
|
295
|
+
const labelDiv = document.createElement("div");
|
|
296
|
+
labelDiv.className = "message-label";
|
|
297
|
+
labelDiv.textContent = label;
|
|
298
|
+
block.appendChild(labelDiv);
|
|
299
|
+
const contentDiv = document.createElement("div");
|
|
300
|
+
contentDiv.className = `message-content ${roleClass}`;
|
|
301
|
+
contentDiv.textContent = msg.message || msg.content || "";
|
|
302
|
+
block.appendChild(contentDiv);
|
|
303
|
+
messagesList.appendChild(block);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
conversationDetail.appendChild(messagesList);
|
|
308
|
+
|
|
309
|
+
// Scroll to bottom
|
|
310
|
+
conversationDetail.scrollTop = conversationDetail.scrollHeight;
|
|
311
|
+
|
|
312
|
+
// Show reply section
|
|
313
|
+
replySection.classList.remove("hidden");
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.error("Error loading conversation detail:", err);
|
|
316
|
+
conversationDetail.innerHTML =
|
|
317
|
+
'<div style="text-align: center; color: #ff6b6b; padding: 2rem;">Unable to load conversation details</div>';
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function sendReply() {
|
|
322
|
+
if (!selectedConversation) return;
|
|
323
|
+
const message = replyInput.value.trim();
|
|
324
|
+
if (!message) return;
|
|
325
|
+
|
|
326
|
+
sendReplyBtn.disabled = true;
|
|
327
|
+
sendReplyBtn.textContent = "Sending...";
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const res = await fetchWithAuth(`/api/conversation/${selectedConversation.id}/reply`, {
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: { "Content-Type": "application/json" },
|
|
333
|
+
body: JSON.stringify({ message }),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
if (!res.ok) throw new Error("Failed to send message");
|
|
337
|
+
|
|
338
|
+
replyInput.value = "";
|
|
339
|
+
// Reload details to show new message
|
|
340
|
+
await renderConversationDetail(selectedConversation);
|
|
341
|
+
// Reload list to update preview/time
|
|
342
|
+
loadConversations();
|
|
343
|
+
} catch (err) {
|
|
344
|
+
console.error("Error sending reply:", err);
|
|
345
|
+
alert("Failed to send reply");
|
|
346
|
+
} finally {
|
|
347
|
+
sendReplyBtn.disabled = false;
|
|
348
|
+
sendReplyBtn.textContent = "Send Reply";
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Filter buttons
|
|
353
|
+
document.querySelectorAll(".filter-btn").forEach((btn) => {
|
|
354
|
+
btn.addEventListener("click", () => {
|
|
355
|
+
document
|
|
356
|
+
.querySelectorAll(".filter-btn")
|
|
357
|
+
.forEach((b) => b.classList.remove("active"));
|
|
358
|
+
btn.classList.add("active");
|
|
359
|
+
currentFilter = btn.dataset.filter;
|
|
360
|
+
renderConversationsList();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Reply button
|
|
365
|
+
if (sendReplyBtn) {
|
|
366
|
+
sendReplyBtn.addEventListener("click", sendReply);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Sign out
|
|
370
|
+
signOutBtn.addEventListener("click", async () => {
|
|
371
|
+
await firebase.auth().signOut();
|
|
372
|
+
window.location.href = "/app";
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Initialize
|
|
376
|
+
firebase.auth().onAuthStateChanged(async (user) => {
|
|
377
|
+
if (!user) {
|
|
378
|
+
authRequired.classList.remove("hidden");
|
|
379
|
+
inboxContainer.classList.add("hidden");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
authRequired.classList.add("hidden");
|
|
384
|
+
inboxContainer.classList.remove("hidden");
|
|
385
|
+
|
|
386
|
+
currentProvider = await loadProvider();
|
|
387
|
+
if (!currentProvider) {
|
|
388
|
+
authRequired.classList.remove("hidden");
|
|
389
|
+
inboxContainer.classList.add("hidden");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
await loadConversations();
|
|
394
|
+
|
|
395
|
+
// Auto-refresh every 30 seconds
|
|
396
|
+
setInterval(loadConversations, 30000);
|
|
397
|
+
});
|