| <!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; |
| } |
| 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; } |
| |
| |
| .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-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 { |
| 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 { |
| 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; } |
| |
| |
| .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> |
|
|
| |
| <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"> |
| |
| <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", () => { |
| |
| 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; |
| |
| 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; |
| |
| |
| 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'; |
| |
| 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(); |
| |
| for (let line of lines) { |
| if (line.startsWith('data: ')) { |
| try { |
| const data = JSON.parse(line.substring(6)); |
| |
| |
| 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; |
| } |
| |
| 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) { |
| |
| } |
| } |
| } |
| } |
| } |
| } 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'; |
| } |
| }); |
| }); |
| |
| |
| window.showFeedbackUI = function() { |
| document.getElementById('main-controls').style.display = 'none'; |
| document.getElementById('feedback-controls').style.display = 'flex'; |
| |
| |
| 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'); |
| |
| 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> |
|
|