nitdaa / templates /index.html
AI Agent
update startup welcome/splash screen messages with new STUP policy details
1ccbf9c
Raw
History Blame Contribute Delete
43 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<title>Healthcare Policy Expert</title>
<meta name="description" content="A mobile-friendly MVP for Healthcare Policy inquiries.">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" defer></script>
<style>
:root {
--primary-color: #0056b3;
--bg-color: #f5f5f5;
--text-color: #333333;
--white: #ffffff;
--border-color: #cccccc;
--focus-outline: #ffbf47;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
height: 100vh;
background: var(--bg-color);
color: var(--text-color);
}
header.banner {
background: var(--primary-color);
color: var(--white);
padding: 1rem;
font-size: 1.25rem;
font-weight: 700;
display: flex;
justify-content: space-between;
align-items: center;
}
header.banner h1 {
margin: 0;
font-size: 1.25rem;
}
.pill {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 700;
color: var(--white);
}
.pill.not-ready {
background: #d32f2f;
animation: blink 1s infinite;
}
.pill.ready {
background: #2e7d32;
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
main {
flex: 1;
padding: 1rem;
overflow-y: auto;
background: var(--white);
margin: 0.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.welcome {
font-weight: 700;
margin-bottom: 1rem;
font-size: 1.125rem;
}
.output {
font-size: 1rem;
line-height: 1.6;
}
.output p { margin-bottom: 1rem; }
footer {
padding: 1rem;
background: var(--white);
border-top: 1px solid var(--border-color);
}
.controls {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
label {
font-weight: 600;
font-size: 0.875rem;
margin-bottom: 0.25rem;
display: inline-block;
}
select, input, button {
padding: 0.75rem;
font-size: 1rem;
border: 1px solid var(--border-color);
border-radius: 6px;
width: 100%;
box-sizing: border-box;
}
select:focus, input:focus, button:focus {
outline: 3px solid var(--focus-outline);
outline-offset: 2px;
}
button {
background: var(--primary-color);
color: var(--white);
border: none;
font-weight: 700;
cursor: pointer;
min-height: 48px; /* accessibility touch target size */
}
button:hover { background: #004494; }
button:disabled { background: #80abc6; cursor: not-allowed; }
.input-group {
display: flex;
gap: 0.5rem;
}
.input-group input { flex: 1; }
.input-group button { width: auto; min-width: 80px; }
/* Feedback styling */
.star-rating {
display: inline-flex;
gap: 5px;
font-size: 1.5rem;
cursor: pointer;
user-select: none;
}
.star {
color: #ccc;
transition: color 0.2s;
}
.star.active {
color: var(--primary-color);
}
/* Star color transitions (1=red, 5=green) */
.star-rating[data-stars="1"] .star:nth-child(1) { color: #f44336; }
.star-rating[data-stars="2"] .star:nth-child(1),
.star-rating[data-stars="2"] .star:nth-child(2) { color: #ff9800; }
.star-rating[data-stars="3"] .star:nth-child(1),
.star-rating[data-stars="3"] .star:nth-child(2),
.star-rating[data-stars="3"] .star:nth-child(3) { color: #ffc107; }
.star-rating[data-stars="4"] .star:nth-child(1),
.star-rating[data-stars="4"] .star:nth-child(2),
.star-rating[data-stars="4"] .star:nth-child(3),
.star-rating[data-stars="4"] .star:nth-child(4) { color: #8bc34a; }
.star-rating[data-stars="5"] .star.active { color: #4caf50; }
.fb-action-btn {
background:none;
border:none;
cursor:pointer;
font-size:1.0rem;
transition: transform 0.2s, filter 0.2s;
padding: 0 4px;
}
/* Milestone graph styling */
.milestone-graph {
display: none;
align-items: center;
margin: 1rem 0;
padding: 1rem;
background: #e9f2fb;
border-radius: 6px;
border-left: 4px solid var(--primary-color);
}
.milestone-dot {
width: 16px;
height: 16px;
background: var(--primary-color);
border-radius: 50%;
animation: pulse 1.5s infinite;
}
.milestone-text {
margin-left: 0.75rem;
font-weight: 700;
color: var(--primary-color);
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.3); opacity: 0.6; }
100% { transform: scale(1); opacity: 1; }
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* CAPTCHA Modal Styling */
#captcha-modal {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-color);
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem;
text-align: center;
}
#captcha-modal .card {
background: var(--white);
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
max-width: 320px;
width: 100%;
}
#captcha-modal h2 { margin-top: 0; color: var(--primary-color); }
#captcha-modal .challenge { font-size: 1.5rem; font-weight: bold; margin: 1rem 0; }
#captcha-error { color: #d32f2f; margin-top: 0.5rem; display: none; font-weight: bold; }
/* Dual LLM Toggle */
.llm-toggle {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
padding: 2px;
margin-right: 12px;
position: relative;
cursor: pointer;
width: 140px;
justify-content: space-between;
}
.llm-toggle span {
z-index: 1;
padding: 2px 0;
font-size: 0.75rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.6);
transition: color 0.3s;
user-select: none;
width: 50%;
text-align: center;
}
.llm-toggle span.active {
color: var(--primary-color);
}
.llm-slider {
position: absolute;
top: 2px;
bottom: 2px;
left: 2px;
width: calc(50% - 2px);
background: var(--white);
border-radius: 14px;
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 0;
}
.llm-toggle[data-mode="assistant"] .llm-slider {
left: 50%;
}
.copy-btn {
position: absolute;
right: 10px;
background: transparent;
color: #888;
border: none;
padding: 6px;
border-radius: 50%;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s, color 0.2s, background 0.2s;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
}
.copy-btn:hover {
opacity: 1;
color: var(--primary-color);
background: #e9f2fb;
}
.copy-bottom {
bottom: 8px;
}
</style>
</head>
<body>
<!-- Security CAPTCHA Overlay -->
<div id="captcha-modal">
<div class="card">
<h2>Security Check</h2>
<p>Please solve this math problem to continue:</p>
<div class="challenge" id="captcha-challenge"></div>
<input type="number" id="captcha-input" placeholder="Your answer" aria-label="Captcha answer">
<div id="captcha-error">Incorrect, try again!</div>
<button id="captcha-btn" style="margin-top: 1rem;">Verify</button>
</div>
</div>
<header class="banner" role="banner">
<div>
<h1>Healthcare Policy Expert</h1>
<div style="font-size: 0.75rem; font-weight: normal; margin-top: 4px; opacity: 0.9;">Agentic AI | RAG System | Edge Compute | Sandboxed</div>
</div>
<div style="display: flex; align-items: center;">
<div id="llm-toggle" class="llm-toggle" data-mode="expert" onclick="toggleLLM()">
<div class="llm-slider"></div>
<span class="active">Expert</span>
<span>Assistant</span>
</div>
<div id="status-pill" class="pill not-ready">Not Ready</div>
</div>
</header>
<div style="background-color: #fff3cd; color: #856404; padding: 0.5rem 1rem; font-size: 0.85rem; text-align: center; border-bottom: 1px solid #ffeeba;">
<strong>Disclaimer:</strong> This application uses AI-generated content. Information provided may not be accurate and should not replace professional advice.
</div>
<main role="main" id="main-content">
<div id="question-display"></div>
<div class="milestone-graph" id="generating-graph" role="status" aria-live="polite">
<div class="milestone-dot" aria-hidden="true"></div>
<div class="milestone-text">Generating answer..</div>
</div>
<section class="output" id="output-area" aria-live="polite" aria-atomic="false">
<!-- Initial Welcome Message -->
<div style="margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 2px solid var(--border-color);">
<div style="font-weight:700; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid #eee; color: var(--primary-color);">🏥 NITDAA Super Top-Up (STUP) Health Insurance Program 2026–27 — Overview</div>
<div class="answer-content">
<p><strong>⚠️ Note:</strong> NIT Durgapur Alumni Association is <strong>NOT</strong> responsible for claim settlement. STUP claims are settled directly by Care Health Insurance Ltd (Group Care 360 product, in-house TPA/claims servicing).</p>
<ul>
<li><strong>👔 Insurance Broker:</strong> Zopper Insurance Brokers</li>
<li><strong>💻 Technology Partner:</strong> Solvy Tech Solutions</li>
<li><strong>🤝 Claims Assistance:</strong> Zopper team supports the full claim journey — <a href="mailto:nitdaahealthplan@zopper.com">nitdaahealthplan@zopper.com</a> | Pradeep: 93196 40944</li>
</ul>
<h3 style="margin-top: 1rem;">📅 Enrollment Window (2026)</h3>
<ul>
<li><strong>STUP Policy:</strong> 10 July 2026 to 10 August 2026</li>
<li><strong>How to Enroll:</strong> Enrol via NITDAA Portal ➡️ Insurance tab: <a href="https://www.nitdaa.org/external_insurance" target="_blank">https://www.nitdaa.org/external_insurance</a></li>
<li><strong>⏱️ Policy Issuance:</strong> Policy issuance is real-time — documents within 4–5 working hours of payment</li>
</ul>
<h3 style="margin-top: 1rem;">👥 Who Can Be Covered</h3>
<ul>
<li><strong>Mandatory Proposer:</strong> Alumni (Self must enroll before Parents or In-Laws)</li>
<li><strong>Eligible Members:</strong> Self, Spouse, Up to 4 Children (up to age 30), Up to 2 Parents, Up to 2 Parents-in-Law — three separate floaters: (1) Self+Spouse+Kids, (2) Parents, (3) In-Laws</li>
<li><strong>Also Eligible:</strong> Life Members and approved Faculty/Staff, Students, Associate NITDAA members via portal</li>
<li><strong>Not Eligible:</strong> Siblings, friends, relatives</li>
<li><strong>Entry Age:</strong> Up to 99 years; once in, renewable for life of the program</li>
<li><strong>Natural Additions:</strong> Marriage/childbirth allowed mid-policy by endorsement — intimate Zopper within 30 days of the event</li>
<li><strong>Pre-policy medical check-up:</strong> Declaration basis only — no health check-up, no medical tests, no medical records</li>
</ul>
<h3 style="margin-top: 1rem;">🛡️ STUP Policy Features (as per enrolment portal — Group Care 360)</h3>
<ul>
<li><strong>Policy Tenure:</strong> 1 Year</li>
<li><strong>In-patient Care:</strong> Up to Sum Insured</li>
<li><strong>Room Rent:</strong> All upper-category rooms excluding suite | <strong>ICU Charges:</strong> No limits</li>
<li><strong>Pre-hospitalization:</strong> 60 days | <strong>Post-hospitalization:</strong> 90 days</li>
<li><strong>Domestic Road Ambulance:</strong> Up to ₹5,000 per hospitalization</li>
<li><strong>Air Ambulance:</strong> Up to ₹2 Lakh per incident</li>
<li><strong>Donor Expenses:</strong> Up to Sum Insured</li>
<li><strong>Domiciliary Hospitalization:</strong> Up to Sum Insured (if exceeding 3 consecutive days)</li>
<li><strong>Day Care Treatment:</strong> All day-care covered (541-procedure list)</li>
<li><strong>AYUSH (Alternative Treatments):</strong> Up to Sum Insured</li>
<li><strong>Consumables:</strong> Covered (as per Annexure in the Brochure)</li>
<li><strong>Modern Treatment:</strong> 50% of Sum Insured; for Sum Insured above ₹30 Lakh, capped at ₹15 Lakh (robotic surgery, immunotherapy, oral chemo, stem cell therapy for hematological conditions, etc.)</li>
<li><strong>Sub-limit:</strong> None | <strong>Co-pay:</strong> Zero | <strong>Covid-19:</strong> Covered</li>
<li><strong>Claims Pay-out:</strong> Cashless within Care network / Reimbursement outside</li>
</ul>
<h3 style="margin-top: 1rem;">⏳ Waiting Periods</h3>
<ul>
<li><strong>Initial 30 days:</strong> Applies (waived for injuries/accidents)</li>
<li><strong>Named Ailments (per Group Care 360):</strong> 12 months from first date of enrollment</li>
<li><strong>Pre-existing Diseases:</strong> 1 year for New Members | 0 days for Renewal Members</li>
</ul>
<h3 style="margin-top: 1rem;">💰 Sum Insured & Deductibles</h3>
<ul>
<li><strong>Sum Insured Options:</strong> ₹10L / 15L / 20L / 25L / 30L / 50L / 75L / 1 Cr / 1.5 Cr / 2 Cr</li>
<li><strong>Deductible Options:</strong> ₹3L / 5L / 7L / 10L / 15L / 20L / 25L — aggregate, crossed only once per policy year per floater</li>
<li><strong>Integration:</strong> Works with any base/employer/individual policy — or none (deductible can be self-paid)</li>
<li><strong>NRI-friendly:</strong> Claims valid for hospitalization within India; foreign-citizen alumni can cover India-resident parents/in-laws</li>
</ul>
<h3 style="margin-top: 1rem;">🧑‍🦽 Optional GPA Rider (Alumni & Spouse only) — ₹1 Cr Individual Cover, Worldwide, Day 1</h3>
<ul>
<li><strong>Accidental Death:</strong> 100% of Sum Insured</li>
<li><strong>PPD/PTD (Disability):</strong> Up to Sum Insured, per Group Care 360 tables</li>
<li><strong>TTD (Temporary Disability):</strong> ₹10,000/week, max 100 weeks</li>
<li><strong>Children's Education:</strong> ₹50,000 per child (max 2 children)</li>
<li><strong>Home Modification / Burns / Funeral:</strong> Home Modification: ₹25,000 | Burns: ₹10,000 | Funeral Expenses: ₹10,000</li>
<li><strong>Tenure & Wait:</strong> 1 Year, zero wait period | 80D-eligible | Alumni must buy for self before adding spouse</li>
</ul>
<h3 style="margin-top: 1rem;">💳 Premium and Payment</h3>
<ul>
<li><strong>Payment Frequency:</strong> Annual only</li>
<li><strong>Tax Benefit:</strong> Section 80D deduction (STUP and GPA both)</li>
<li><strong>Premium Rating Basis:</strong> Age of the oldest family member in the floater, group claims ratio; City-neutral</li>
<li><strong>Payment Flow:</strong> Member pays via NITDAA portal ➡️ Collected by Solvy Tech (Zopper) ➡️ Transferred to Care Health Insurance. Receipt and policy issued by the Insurance Company, NOT by NITDAA</li>
</ul>
<h3 style="margin-top: 1rem;">🧾 GST Applicability</h3>
<ul>
<li>Group policies (including NITDAA): <strong>18% GST applies</strong></li>
<li><strong>Total Cost:</strong> Base Premium + 18% GST</li>
<li><strong>Note:</strong> GST was removed only for individual/family retail health insurance, not group policies</li>
</ul>
<h3 style="margin-top: 1rem;">💡 Ideal Coverage Strategy</h3>
<ul>
<li>Use a base cover (3L–5L) as the STUP deductible, with STUP on top (SI up to ₹2 Cr). Higher deductibles (7/10/15/20/25L) lower the premium — a 10L deductible pairs well with a 10L base policy</li>
<li>Base policy <strong>NOT</strong> mandatory — deductible can be met out of pocket or through any insurer</li>
<li>If employer cover lapses (retirement/job change), STUP is unaffected — add a backup base cover</li>
</ul>
<h3 style="margin-top: 1rem;">🔄 Recommended Transition Strategy</h3>
<ul>
<li><strong>Do NOT</strong> cancel your existing policy when joining. Maintain overlap for at least 1 year</li>
<li><strong>Year 1:</strong> PED and named-ailment claims (12-month wait) go to the existing policy; accident claims covered from day 1</li>
<li><strong>After first renewal:</strong> PED wait drops to zero; then reassess the old policy</li>
<li><strong>⚠️ Critical:</strong> Skip a renewal and you cannot re-enter the program ever — continuity is everything</li>
</ul>
<h3 style="margin-top: 1rem;">👨‍👩‍👧‍👦 Coverage Continuity for Family</h3>
<ul>
<li>If the Alumni passes away, family remains fully covered until the next renewal</li>
<li>For subsequent renewals, surviving family must contact the NITDAA Alumni team for portal access and renew annually</li>
<li>If both spouses are alumni, the survivor can re-enrol independently as proposer</li>
</ul>
<h3 style="margin-top: 1rem;">👴👵 Parents and Parents-in-Law</h3>
<ul>
<li>Separate floaters under the alumni's policy — each with its own deductible, crossed independently</li>
<li>Parent claims paid via employer group insurance still count toward the STUP deductible</li>
</ul>
<h3 style="margin-top: 1rem;">🏆 Why Trust It</h3>
<ul>
<li>Care Health claims settlement ratio: <strong>97%</strong> | IRDA-regulated | Cashless at all Care network hospitals: <a href="https://www.careinsurance.com/health-plan-network-hospitals.html" target="_blank">careinsurance.com/health-plan-network-hospitals.html</a></li>
<li><strong>📧 Queries:</strong> <a href="mailto:nitdaahealthplan@zopper.com">nitdaahealthplan@zopper.com</a></li>
</ul>
</div>
</div>
</section>
</main>
<footer role="contentinfo">
<div class="controls">
<div id="main-controls">
<div style="margin-bottom: 0.5rem;">
<label for="quick-questions">Quick Questions</label>
<select id="quick-questions" aria-label="Select a quick question">
<option value="">-- Select a Quick Question --</option>
<option value="What is the waiting period for Pre-Existing Diseases (PEDs)?">1. What is the waiting period for PEDs?</option>
<option value="What are the available Sum Insured (SI) options?">2. Available Sum Insured options?</option>
<option value="Can I claim if I have employer/retail insurance?">3. Claiming with other insurance?</option>
<option value="Are there sub-limits on room rent and treatments?">4. Sub-limits on room rent?</option>
<option value="When must I notify for cashless claims?">5. Notifying for cashless claims?</option>
<option value="How fast is cashless pre-auth?">6. Cashless pre-auth speed?</option>
<option value="What is the reimbursement document deadline and settlement time?">7. Reimbursement deadlines?</option>
<option value="Will premium increase if I make a claim this year?">8. Claim impact on premium?</option>
<option value="Who handles policy enrollment and claim support?">9. Claim support contacts?</option>
<option value="What is the cancellation policy and premium refund?">10. Cancellation & refunds?</option>
</select>
</div>
<div>
<label for="question-input" class="sr-only">Type your question</label>
<div class="input-group">
<button type="button" id="lang-switch-btn" aria-label="Switch keyboard language" title="Switch Keyboard Language" style="min-width: 50px; width: 50px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; background: #f0f0f0; border: 1px solid var(--border-color);" onclick="switchKeyboardLanguage()">🌐</button>
<input type="text" id="question-input" placeholder="Ask a question..." aria-required="true">
<button id="send-btn" aria-label="Send question">Send</button>
</div>
</div>
</div>
<div id="feedback-thank-you" style="display: none; color: #2e7d32; font-weight: bold; text-align: center; margin-bottom: 10px;">
Thank you! Your feedback has been logged.
</div>
<div id="feedback-controls" style="display: none; flex-direction: column; gap: 10px;">
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
<span style="font-size: 0.9rem; font-weight: bold; color: var(--primary-color);">Provide Feedback:</span>
<button id="fb-up-btn" class="fb-action-btn" onclick="submitFeedbackAct('positive')" aria-label="Thumbs up">👍</button>
<button id="fb-down-btn" class="fb-action-btn" onclick="submitFeedbackAct('negative')" aria-label="Thumbs down">👎</button>
<div class="star-rating" id="star-rating-container" data-stars="0">
<span class="star" data-val="1" onclick="submitFeedbackAct('star', 1)"></span>
<span class="star" data-val="2" onclick="submitFeedbackAct('star', 2)"></span>
<span class="star" data-val="3" onclick="submitFeedbackAct('star', 3)"></span>
<span class="star" data-val="4" onclick="submitFeedbackAct('star', 4)"></span>
<span class="star" data-val="5" onclick="submitFeedbackAct('star', 5)"></span>
</div>
</div>
<div class="input-group">
<input type="text" id="fb-text-input" placeholder="Optional feedback..." aria-label="Optional feedback text">
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button onclick="hideFeedbackUI()" style="padding: 8px 15px; background: #999; color: white; border: none; border-radius: 4px; cursor: pointer;">Cancel</button>
<button onclick="submitFeedbackText()" style="padding: 8px 15px; background: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">Submit</button>
</div>
</div>
</div>
</footer>
<script>
const LOG_PREFIX = '[HealthExpert Diag]';
const diag = {
info: (...a) => console.info( `${LOG_PREFIX}`, ...a),
warn: (...a) => console.warn( `${LOG_PREFIX}`, ...a),
error: (...a) => console.error(`${LOG_PREFIX}`, ...a)
};
let notReadyStartTime = null;
const NOT_READY_TIMEOUT_MS = 300 * 1000;
let sessionToken = localStorage.getItem('he_session_token');
if (!sessionToken) {
sessionToken = 'sess_' + Math.random().toString(36).substring(2, 11);
localStorage.setItem('he_session_token', sessionToken);
}
const SESSION_TOKEN = sessionToken;
let currentFeedbackJobId = null;
document.addEventListener("DOMContentLoaded", () => {
// --- CAPTCHA Logic ---
const captchaModal = document.getElementById('captcha-modal');
const captchaChallenge = document.getElementById('captcha-challenge');
const captchaInput = document.getElementById('captcha-input');
const captchaBtn = document.getElementById('captcha-btn');
const captchaError = document.getElementById('captcha-error');
let captchaExpected = 0;
function generateCaptcha() {
const num1 = Math.floor(Math.random() * 10) + 1;
const num2 = Math.floor(Math.random() * 10) + 1;
captchaExpected = num1 + num2;
captchaChallenge.textContent = `${num1} + ${num2} = ?`;
captchaInput.value = '';
}
function verifyCaptcha() {
const val = parseInt(captchaInput.value, 10);
if (val === captchaExpected) {
captchaModal.style.display = 'none';
sessionStorage.setItem('captcha_solved', 'true');
pollStatus();
} else {
captchaError.style.display = 'block';
generateCaptcha();
}
}
captchaBtn.addEventListener('click', verifyCaptcha);
captchaInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') verifyCaptcha();
});
if (sessionStorage.getItem('captcha_solved') === 'true') {
captchaModal.style.display = 'none';
pollStatus();
} else {
generateCaptcha();
}
// ---------------------
const quickSelect = document.getElementById('quick-questions');
const questionInput = document.getElementById('question-input');
const sendBtn = document.getElementById('send-btn');
const outputArea = document.getElementById('output-area');
const questionDisplay = document.getElementById('question-display');
const genGraph = document.getElementById('generating-graph');
const statusPill = document.getElementById('status-pill');
let wasOffline = false;
async function pollStatus() {
try {
const res = await fetch('/api/status', {
headers: { 'X-Session-Token': SESSION_TOKEN }
});
const data = await res.json();
if (wasOffline) {
window.location.reload();
return;
}
const vecOk = (data.vector_db?.chunks ?? -1) >= 0;
const genOk = data.nvidia_llm?.online;
const embedOk = data.embed_llm?.online;
// Check for active ingestion / graphdb tasks via our system info endpoint as well
let isIngesting = false;
try {
const sysRes = await fetch('/api/sysinfo');
const sysData = await sysRes.json();
isIngesting = sysData.active_graph_tasks > 0;
} catch(e) {}
if (vecOk && genOk && embedOk && !isIngesting) {
statusPill.className = 'pill ready';
statusPill.textContent = 'Ready';
notReadyStartTime = null;
diag.info('Status Check -> ALL READY');
} else {
statusPill.className = 'pill not-ready';
statusPill.textContent = 'Not Ready';
diag.info(`Status Check -> Vector: ${vecOk}, GenLLM: ${genOk}, EmbedLLM: ${embedOk}, GraphIngesting: ${isIngesting}`);
if (notReadyStartTime === null) {
notReadyStartTime = Date.now();
} else {
const elapsed = Date.now() - notReadyStartTime;
if (elapsed > NOT_READY_TIMEOUT_MS) {
diag.warn(`CRITICAL: App has been in Not Ready state for ${Math.floor(elapsed/1000)} seconds!`);
}
}
}
} catch (e) {
wasOffline = true;
statusPill.className = 'pill not-ready';
statusPill.textContent = 'Not Ready';
diag.error('Status polling failed:', e);
if (notReadyStartTime === null) {
notReadyStartTime = Date.now();
}
}
setTimeout(pollStatus, 5000);
}
quickSelect.addEventListener('change', () => {
if (quickSelect.value) {
questionInput.value = quickSelect.value;
sendBtn.click();
}
});
questionInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
sendBtn.click();
}
});
sendBtn.addEventListener('click', async () => {
const q = questionInput.value.trim();
if (!q) return;
// Create a new container for this Q&A pair so we can append
const qaContainer = document.createElement('div');
qaContainer.className = 'qa-container';
qaContainer.style.marginBottom = '2rem';
qaContainer.style.paddingBottom = '1rem';
qaContainer.style.borderBottom = '2px solid var(--border-color)';
qaContainer.style.position = 'relative';
qaContainer.innerHTML = `
<div class="q-text" style="font-weight:700; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid #eee; color: var(--primary-color);">Q: ${q}</div>
<div class="answer-content"></div>
<button class="copy-btn copy-bottom" onclick="copyQA(this)" style="display:none;" title="Copy to clipboard" aria-label="Copy response">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
`;
outputArea.appendChild(qaContainer);
const answerContent = qaContainer.querySelector('.answer-content');
genGraph.style.display = 'flex';
// Move genGraph visually below the current question
outputArea.appendChild(genGraph);
const milestoneText = genGraph.querySelector('.milestone-text');
if (milestoneText) milestoneText.setAttribute('data-status', 'Generating answer..');
let timerInterval = null;
let elapsedSec = 0;
const updateTimer = () => {
elapsedSec += 0.1;
if (milestoneText) {
const baseText = milestoneText.getAttribute('data-status') || 'Generating answer..';
milestoneText.textContent = `${baseText} (${elapsedSec.toFixed(1)}s)`;
}
};
updateTimer();
timerInterval = setInterval(updateTimer, 100);
sendBtn.disabled = true;
sendBtn.setAttribute('aria-busy', 'true');
try {
diag.info(`Submitting query: "${q}"`);
const t = document.getElementById('llm-toggle');
const llmMode = t ? t.getAttribute('data-mode') : 'expert';
const payload = {
query: q,
use_vector: true,
use_graph: true,
use_bm25: true,
top_k: 10,
max_tokens: 2048,
cpu_threads: 16,
llm_mode: llmMode
};
const startRes = await fetch('/api/query/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Session-Token': SESSION_TOKEN },
body: JSON.stringify(payload)
});
if (!startRes.ok) {
throw new Error(`Network response was not ok: ${startRes.status}`);
}
const startData = await startRes.json();
const jobId = startData.job_id;
diag.info('Generation started. Streaming chunks...');
let jobDone = false;
let currentOffset = 0;
let answer = '';
while (!jobDone) {
try {
const res = await fetch(`/api/query/stream/${jobId}?offset=${currentOffset}`, {
headers: { 'X-Session-Token': SESSION_TOKEN }
});
if (!res.ok) {
if (res.status === 404) throw new Error("Job not found or expired on server.");
throw new Error(`Stream network response was not ok: ${res.status}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (!jobDone) {
const { value, done: readerDone } = await reader.read();
if (readerDone) break;
if (value) {
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop(); // keep the last incomplete chunk in buffer
for (let line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.substring(6));
// Successfully received an event
currentOffset++;
if (data.chunk) {
genGraph.style.display = 'none';
answer += data.chunk;
diag.info(`Received chunk: ${data.chunk}`);
if (window.marked) {
answerContent.innerHTML = marked.parse(answer);
} else {
answerContent.textContent = answer;
}
// Auto-scroll to bottom
window.scrollTo(0, document.body.scrollHeight);
} else if (data.status) {
diag.info(`Status update:`, data.status);
if (milestoneText) {
const msg = typeof data.status === 'string' ? data.status : (data.status.message || 'Processing...');
milestoneText.setAttribute('data-status', msg);
}
} else if (data.metrics) {
diag.info(`Generation metrics:`, data.metrics);
const metricsHtml = `
<div style="font-size: 0.85rem; color: #666; margin-top: 15px; border-top: 1px dashed var(--border-color); padding-top: 10px;">
<span style="font-weight: bold; margin-right: 10px;">${data.metrics.model_name || 'LLM'}</span>
Analysis took ${data.metrics.time_seconds?.toFixed(1) || '?'} seconds.
Tokens: ${data.metrics.tokens_in || '?'} in, ${data.metrics.tokens_out || '?'} out.
</div>
`;
answerContent.innerHTML += metricsHtml;
window.scrollTo(0, document.body.scrollHeight);
} else if (data.done) {
diag.info('Generation complete.');
const bottomCopy = qaContainer.querySelector('.copy-bottom');
if (bottomCopy) bottomCopy.style.display = 'block';
currentFeedbackJobId = jobId;
showFeedbackUI();
jobDone = true;
break;
} else if (data.error) {
genGraph.style.display = 'none';
diag.error(`Backend error: ${data.error}`);
answerContent.innerHTML += `<p style="color:#d32f2f; font-weight:bold;">Error: ${data.error}</p>`;
jobDone = true;
break;
}
} catch (e) {
// Ignore parse errors on incomplete chunks
}
}
}
}
}
} catch (err) {
if (err.message.includes("Job not found") || jobDone) {
throw err;
}
diag.warn('Network dropped, reconnecting to job...', err);
await new Promise(r => setTimeout(r, 1500));
}
}
} catch (e) {
genGraph.style.display = 'none';
diag.error(`Query execution failed: ${e.message}`);
outputArea.innerHTML += `<p style="color:#d32f2f; font-weight:bold; margin-top:1rem;">Error: ${e.message}</p>`;
} finally {
if (timerInterval) clearInterval(timerInterval);
questionInput.value = '';
quickSelect.value = '';
sendBtn.disabled = false;
sendBtn.setAttribute('aria-busy', 'false');
genGraph.style.display = 'none';
}
});
});
// Feedback Logic
window.showFeedbackUI = function() {
document.getElementById('main-controls').style.display = 'none';
document.getElementById('feedback-controls').style.display = 'flex';
// Reset UI
document.getElementById('fb-up-btn').style.transform = 'scale(1)';
document.getElementById('fb-up-btn').style.filter = 'none';
document.getElementById('fb-down-btn').style.transform = 'scale(1)';
document.getElementById('fb-down-btn').style.filter = 'none';
const starContainer = document.getElementById('star-rating-container');
starContainer.setAttribute('data-stars', '0');
starContainer.querySelectorAll('.star').forEach(s => s.classList.remove('active'));
document.getElementById('fb-text-input').value = '';
};
window.hideFeedbackUI = function() {
document.getElementById('feedback-controls').style.display = 'none';
document.getElementById('main-controls').style.display = 'block';
};
window.submitFeedbackAct = async function(actionType, value) {
if (!currentFeedbackJobId) return;
let payload = { job_id: currentFeedbackJobId };
if (actionType === 'positive' || actionType === 'negative') {
payload.rating = actionType;
const upBtn = document.getElementById('fb-up-btn');
const downBtn = document.getElementById('fb-down-btn');
upBtn.style.transform = actionType === 'positive' ? 'scale(1.2)' : 'scale(1)';
upBtn.style.filter = actionType === 'positive' ? 'drop-shadow(0 0 5px #4caf50)' : 'none';
downBtn.style.transform = actionType === 'negative' ? 'scale(1.2)' : 'scale(1)';
downBtn.style.filter = actionType === 'negative' ? 'drop-shadow(0 0 5px #f44336)' : 'none';
} else if (actionType === 'star') {
payload.stars = value;
const starContainer = document.getElementById('star-rating-container');
starContainer.setAttribute('data-stars', value);
starContainer.querySelectorAll('.star').forEach((s, idx) => {
if (idx < value) s.classList.add('active');
else s.classList.remove('active');
});
}
try {
await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Session-Token': SESSION_TOKEN },
body: JSON.stringify(payload)
});
} catch (e) {
console.error("Failed to submit feedback", e);
}
};
window.submitFeedbackText = async function() {
if (!currentFeedbackJobId) return;
const text = document.getElementById('fb-text-input').value.trim();
if (text) {
try {
await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Session-Token': SESSION_TOKEN },
body: JSON.stringify({ job_id: currentFeedbackJobId, text: text })
});
} catch (e) {
console.error("Failed to submit feedback text", e);
}
}
hideFeedbackUI();
const tyDiv = document.getElementById('feedback-thank-you');
tyDiv.style.display = 'block';
setTimeout(() => { tyDiv.style.display = 'none'; }, 3000);
};
window.toggleLLM = function() {
const t = document.getElementById('llm-toggle');
const spans = t.querySelectorAll('span');
if (t.getAttribute('data-mode') === 'expert') {
t.setAttribute('data-mode', 'assistant');
spans[0].classList.remove('active');
spans[1].classList.add('active');
} else {
t.setAttribute('data-mode', 'expert');
spans[1].classList.remove('active');
spans[0].classList.add('active');
}
};
window.copyQA = function(btn) {
const container = btn.closest('.qa-container');
if (!container) return;
const qText = container.querySelector('.q-text') ? container.querySelector('.q-text').innerText : '';
const aText = container.querySelector('.answer-content') ? container.querySelector('.answer-content').innerText : '';
const fullText = qText + "\n\n" + aText;
navigator.clipboard.writeText(fullText).then(() => {
const originalHTML = btn.innerHTML;
btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#4caf50" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
setTimeout(() => {
btn.innerHTML = originalHTML;
}, 2000);
}).catch(err => {
console.error('Failed to copy text: ', err);
});
};
window.switchKeyboardLanguage = function() {
const input = document.getElementById('question-input');
// Toggling the lang attribute may hint the mobile OS keyboard to switch languages/dictionaries
const originalPlaceholder = input.placeholder.includes('Hint') ? 'Ask a question...' : input.placeholder;
if (!input.lang || input.lang === 'en') {
input.lang = 'hi';
input.placeholder = 'Hindi Keyboard Hint...';
} else if (input.lang === 'hi') {
input.lang = 'bn';
input.placeholder = 'Bengali Keyboard Hint...';
} else {
input.lang = 'en';
input.placeholder = 'English Keyboard Hint...';
}
input.focus();
setTimeout(() => {
if (input.placeholder.includes('Hint')) {
input.placeholder = originalPlaceholder;
}
}, 2000);
};
</script>
</body>
</html>