Skip to content

Commit 2e900f9

Browse files
committed
feat: Add styled error alert cards for token quota and BookStack connection failures
- Server now classifies errors before sending over SSE: bookstack_error and quota_error types - BookStack network failures (ECONNREFUSED, ENOTFOUND, ETIMEDOUT, 5xx) surface as bookstack_error - OpenAI quota / rate-limit errors (429, insufficient_quota) surface as quota_error - Frontend appendErrorCard() helper renders themed alert cards in the chat stream - Quota error: amber card with token exhausted message and IT admin contact note - Connection error: red card with VPN hint 'Check your Zters VPN connection' - Generic error: red card fallback - All error cards adapt to dark and light mode via scoped CSS overrides
1 parent b51abaf commit 2e900f9

3 files changed

Lines changed: 164 additions & 4 deletions

File tree

chatbot/public/app.js

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,44 @@ function appendMessage(role, content, html = false) {
115115
return div;
116116
}
117117

118+
// Styled error alert cards
119+
function appendErrorCard(type) {
120+
if (welcomeScreen.style.display !== 'none') {
121+
welcomeScreen.style.display = 'none';
122+
}
123+
124+
let variant, icon, title, desc;
125+
126+
if (type === 'quota_error') {
127+
variant = 'quota';
128+
icon = '⚠️';
129+
title = 'AI Token Quota Reached';
130+
desc = 'The OpenAI token quota for this account has been exhausted. Please contact your Zters IT administrator to top up the API credits.';
131+
} else if (type === 'bookstack_error') {
132+
variant = 'connection';
133+
icon = '🔌';
134+
title = 'Cannot Connect to BookStack';
135+
desc = 'The assistant is unable to reach the Zters knowledge base. Please check your <strong>Zters VPN connection</strong> and try again.';
136+
} else {
137+
variant = 'generic';
138+
icon = '❌';
139+
title = 'Something went wrong';
140+
desc = 'An unexpected error occurred. Please try again or contact Zters IT support if the problem persists.';
141+
}
142+
143+
const card = document.createElement('div');
144+
card.className = `error-card ${variant}`;
145+
card.innerHTML = `
146+
<span class="error-icon">${icon}</span>
147+
<div class="error-body">
148+
<span class="error-title">${title}</span>
149+
<span class="error-desc">${desc}</span>
150+
</div>
151+
`;
152+
messagesWrapper.appendChild(card);
153+
scrollToBottom();
154+
}
155+
118156
function updateStatus(text) {
119157
let indicator = document.getElementById('current-status');
120158
if (!indicator) {
@@ -281,10 +319,15 @@ async function handleChatRequest(message, isSystemCommand = false) {
281319
botMsgDiv.innerHTML = marked.parse(fullResponse);
282320
scrollToBottom();
283321
}
322+
else if (data.type === 'quota_error' || data.type === 'bookstack_error') {
323+
removeStatus();
324+
botMsgDiv.remove(); // remove empty bot placeholder
325+
appendErrorCard(data.type);
326+
}
284327
else if (data.type === 'error') {
285328
removeStatus();
286-
fullResponse += `\n\n*Error: ${data.content}*`;
287-
botMsgDiv.innerHTML = marked.parse(fullResponse);
329+
botMsgDiv.remove();
330+
appendErrorCard('error');
288331
}
289332
else if (data.type === 'done') {
290333
removeStatus();
@@ -305,7 +348,8 @@ async function handleChatRequest(message, isSystemCommand = false) {
305348
}
306349
} catch (error) {
307350
removeStatus();
308-
botMsgDiv.innerHTML = `<span style="color:red">Connection error: ${error.message}</span>`;
351+
botMsgDiv.remove();
352+
appendErrorCard('bookstack_error');
309353
} finally {
310354
sendBtn.disabled = false;
311355
input.focus();

chatbot/public/styles.css

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,3 +800,91 @@ a:hover { text-decoration: underline; }
800800
transition: all 0.2s;
801801
}
802802
.view-sources-btn:hover { background: rgba(255,255,255,0.05); color: var(--text-primary); }
803+
804+
/* ── Error Alert Cards ─────────────────────────────────────────────── */
805+
.error-card {
806+
align-self: flex-start;
807+
width: 100%;
808+
max-width: 680px;
809+
border-radius: 10px;
810+
padding: 14px 18px;
811+
margin-bottom: 20px;
812+
display: flex;
813+
gap: 14px;
814+
align-items: flex-start;
815+
animation: fadeIn 0.3s ease;
816+
border: 1px solid transparent;
817+
}
818+
819+
.error-card .error-icon {
820+
font-size: 1.3rem;
821+
flex-shrink: 0;
822+
margin-top: 1px;
823+
}
824+
825+
.error-card .error-body {
826+
display: flex;
827+
flex-direction: column;
828+
gap: 4px;
829+
}
830+
831+
.error-card .error-title {
832+
font-weight: 600;
833+
font-size: 0.95rem;
834+
}
835+
836+
.error-card .error-desc {
837+
font-size: 0.875rem;
838+
line-height: 1.5;
839+
opacity: 0.9;
840+
}
841+
842+
/* — Quota / token limit — amber */
843+
.error-card.quota {
844+
background: rgba(251, 191, 36, 0.12);
845+
border-color: rgba(251, 191, 36, 0.4);
846+
border-left: 4px solid #f59e0b;
847+
color: #fbbf24;
848+
}
849+
.error-card.quota .error-title { color: #fbbf24; }
850+
.error-card.quota .error-desc { color: #fde68a; }
851+
852+
/* — BookStack connection — red */
853+
.error-card.connection {
854+
background: rgba(239, 68, 68, 0.12);
855+
border-color: rgba(239, 68, 68, 0.35);
856+
border-left: 4px solid #ef4444;
857+
color: #f87171;
858+
}
859+
.error-card.connection .error-title { color: #f87171; }
860+
.error-card.connection .error-desc { color: #fca5a5; }
861+
862+
/* — Generic error — same red palette */
863+
.error-card.generic {
864+
background: rgba(239, 68, 68, 0.12);
865+
border-color: rgba(239, 68, 68, 0.35);
866+
border-left: 4px solid #ef4444;
867+
color: #f87171;
868+
}
869+
.error-card.generic .error-title { color: #f87171; }
870+
.error-card.generic .error-desc { color: #fca5a5; }
871+
872+
/* Light-mode overrides for error cards */
873+
body.light-theme .error-card.quota {
874+
background: #fffbeb;
875+
border-color: #fcd34d;
876+
color: #92400e;
877+
}
878+
body.light-theme .error-card.quota .error-title { color: #92400e; }
879+
body.light-theme .error-card.quota .error-desc { color: #b45309; }
880+
881+
body.light-theme .error-card.connection,
882+
body.light-theme .error-card.generic {
883+
background: #fef2f2;
884+
border-color: #fca5a5;
885+
color: #991b1b;
886+
}
887+
body.light-theme .error-card.connection .error-title,
888+
body.light-theme .error-card.generic .error-title { color: #991b1b; }
889+
body.light-theme .error-card.connection .error-desc,
890+
body.light-theme .error-card.generic .error-desc { color: #b91c1c; }

chatbot/server.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ async function searchBookStack(query, count = 10) {
3939
return res.data.data || [];
4040
} catch (err) {
4141
console.error('BookStack search error:', err.message);
42+
// Surface connection failures with a typed error so the caller can report them
43+
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT' || (err.response && err.response.status >= 500)) {
44+
const e = new Error('Cannot reach the BookStack server');
45+
e.type = 'bookstack_error';
46+
throw e;
47+
}
4248
return [];
4349
}
4450
}
@@ -318,7 +324,29 @@ ${contextBlock}`;
318324
res.end();
319325
} catch (err) {
320326
console.error('Chat error:', err);
321-
res.write(`data: ${JSON.stringify({ type: 'error', content: err.message })}\n\n`);
327+
328+
// Classify the error for the frontend
329+
let errorType = 'error';
330+
let errorMsg = err.message;
331+
332+
if (err.type === 'bookstack_error') {
333+
errorType = 'bookstack_error';
334+
errorMsg = 'Cannot reach the BookStack server.';
335+
} else if (
336+
err?.status === 429 ||
337+
err?.error?.type === 'insufficient_quota' ||
338+
(err.message && (err.message.includes('quota') || err.message.includes('rate limit') || err.message.includes('insufficient_quota') || err.message.includes('429')))
339+
) {
340+
errorType = 'quota_error';
341+
errorMsg = 'OpenAI token quota exceeded.';
342+
} else if (
343+
err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT'
344+
) {
345+
errorType = 'bookstack_error';
346+
errorMsg = 'Cannot reach the BookStack server.';
347+
}
348+
349+
res.write(`data: ${JSON.stringify({ type: errorType, content: errorMsg })}\n\n`);
322350
res.end();
323351
}
324352
});

0 commit comments

Comments
 (0)