| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Irminsul β Genshin Impact AI Assistant</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600&family=EB+Garamond:ital,wght@0,400;0,500;1,400&family=JetBrains+Mono:wght@300;400&display=swap" rel="stylesheet"> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script> |
| <style> |
| :root { |
| --bg: #060d08; |
| --bg2: #0a1410; |
| --surface: rgba(8,18,12,0.88); |
| --surface2: rgba(12,24,16,0.92); |
| --border: rgba(80,180,100,0.12); |
| --border2: rgba(80,180,100,0.22); |
| --dendro: #7ecb6a; |
| --dendro2: #a8e090; |
| --dendro3: #c8f0b0; |
| --parchment: #c8b888; |
| --parchment2:#e0d0a8; |
| --teal: #4ab890; |
| --text: #d8e8d0; |
| --text2: #7a9878; |
| --text3: #3a5038; |
| --green: #5dd68c; |
| --red: #e87878; |
| --amber: #d8a84a; |
| --gold: #d4af37; |
| --mono: 'JetBrains Mono', monospace; |
| --serif: 'EB Garamond', serif; |
| --display: 'Cinzel', serif; |
| } |
| |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| body { |
| background: var(--bg); |
| color: var(--text); |
| font-family: var(--serif); |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| overflow-x: hidden; |
| } |
| |
| canvas#bg { position: fixed; inset: 0; z-index: 0; pointer-events: none; } |
| |
| |
| .atmo { |
| position: fixed; |
| border-radius: 50%; |
| pointer-events: none; |
| z-index: 0; |
| filter: blur(80px); |
| } |
| .atmo-1 { |
| width: 700px; height: 500px; |
| top: -100px; left: -100px; |
| background: radial-gradient(ellipse, rgba(40,100,50,0.18) 0%, transparent 70%); |
| } |
| .atmo-2 { |
| width: 500px; height: 400px; |
| top: 30%; right: -80px; |
| background: radial-gradient(ellipse, rgba(60,160,80,0.1) 0%, transparent 70%); |
| } |
| .atmo-3 { |
| width: 600px; height: 300px; |
| bottom: 0; left: 20%; |
| background: radial-gradient(ellipse, rgba(30,80,40,0.14) 0%, transparent 70%); |
| } |
| |
| header { |
| position: relative; z-index: 10; |
| padding: 20px 48px; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| border-bottom: 1px solid var(--border); |
| backdrop-filter: blur(20px); |
| background: rgba(6,13,8,0.7); |
| } |
| |
| .logo { display: flex; align-items: center; gap: 14px; } |
| |
| .logo-mark { |
| width: 38px; height: 38px; |
| border-radius: 50%; |
| border: 1px solid rgba(126,203,106,0.3); |
| background: radial-gradient(circle at 40% 35%, rgba(80,180,80,0.15), rgba(6,13,8,0.95)); |
| display: flex; align-items: center; justify-content: center; |
| font-size: 18px; |
| box-shadow: 0 0 16px rgba(80,200,80,0.12), inset 0 0 12px rgba(80,200,80,0.06); |
| animation: runeGlow 3s ease-in-out infinite; |
| } |
| |
| @keyframes runeGlow { |
| 0%,100% { box-shadow: 0 0 16px rgba(80,200,80,0.12), inset 0 0 12px rgba(80,200,80,0.06); } |
| 50% { box-shadow: 0 0 28px rgba(80,200,80,0.25), inset 0 0 16px rgba(80,200,80,0.12); } |
| } |
| |
| .logo-text { |
| font-family: var(--display); |
| font-size: 17px; font-weight: 500; |
| color: var(--dendro2); |
| letter-spacing: 0.08em; |
| } |
| |
| .logo-sub { |
| font-size: 10px; color: var(--text3); |
| font-family: var(--mono); |
| margin-top: 2px; letter-spacing: 0.04em; |
| } |
| |
| .status-pill { |
| display: flex; align-items: center; gap: 7px; |
| padding: 5px 13px; border-radius: 20px; |
| border: 1px solid var(--border); |
| background: rgba(8,18,12,0.8); |
| font-family: var(--mono); font-size: 11px; color: var(--text2); |
| backdrop-filter: blur(8px); |
| } |
| |
| .status-dot { |
| width: 6px; height: 6px; border-radius: 50%; |
| background: var(--text3); transition: background 0.3s; |
| } |
| .status-dot.online { background: var(--green); box-shadow: 0 0 7px var(--green); } |
| .status-dot.error { background: var(--red); } |
| .status-dot.loading { background: var(--amber); animation: pulse 1s ease-in-out infinite; } |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} } |
| |
| main { |
| position: relative; z-index: 10; |
| flex: 1; display: flex; flex-direction: column; |
| max-width: 800px; width: 100%; |
| margin: 0 auto; padding: 52px 40px 0; |
| } |
| |
| |
| .hero { |
| margin-bottom: 44px; |
| text-align: center; |
| animation: fadeUp 0.7s ease both; |
| } |
| |
| @keyframes fadeUp { |
| from { opacity:0; transform: translateY(14px); } |
| to { opacity:1; transform: translateY(0); } |
| } |
| |
| .rune-row { |
| display: flex; align-items: center; justify-content: center; |
| gap: 12px; margin-bottom: 18px; |
| font-size: 14px; color: var(--dendro); |
| opacity: 0.6; letter-spacing: 0.3em; |
| } |
| |
| .rune-row span { animation: runeFlicker 4s ease-in-out infinite; } |
| .rune-row span:nth-child(2) { animation-delay: 0.8s; } |
| .rune-row span:nth-child(3) { animation-delay: 1.6s; } |
| @keyframes runeFlicker { |
| 0%,100%{opacity:0.5} 50%{opacity:1;text-shadow:0 0 8px var(--dendro);} |
| } |
| |
| .hero h1 { |
| font-family: var(--display); |
| font-size: 40px; font-weight: 500; |
| color: var(--parchment2); |
| letter-spacing: 0.06em; line-height: 1.2; |
| margin-bottom: 8px; |
| text-shadow: 0 0 60px rgba(100,200,80,0.15); |
| } |
| |
| .hero h1 em { |
| font-style: normal; |
| color: var(--dendro2); |
| text-shadow: 0 0 30px rgba(126,203,106,0.5); |
| } |
| |
| .hero-sub { |
| font-size: 16px; font-style: italic; |
| color: var(--text2); line-height: 1.75; |
| max-width: 480px; margin: 0 auto 20px; |
| } |
| |
| .model-tag { |
| display: inline-flex; align-items: center; gap: 8px; |
| padding: 5px 16px; border-radius: 3px; |
| border: 1px solid rgba(126,203,106,0.2); |
| background: rgba(40,80,30,0.2); |
| font-family: var(--mono); font-size: 11px; |
| color: var(--dendro); letter-spacing: 0.04em; |
| } |
| |
| |
| .ornament { |
| display: flex; align-items: center; |
| gap: 12px; margin-bottom: 36px; opacity: 0.25; |
| } |
| .ornament-line { |
| flex: 1; height: 1px; |
| background: linear-gradient(90deg, transparent, var(--dendro), transparent); |
| } |
| .ornament-glyph { |
| font-size: 12px; color: var(--dendro); |
| letter-spacing: 4px; |
| } |
| |
| |
| .query-box { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 4px; |
| overflow: hidden; |
| transition: border-color 0.3s, box-shadow 0.3s; |
| animation: fadeUp 0.7s 0.1s ease both; |
| backdrop-filter: blur(20px); |
| position: relative; |
| } |
| |
| |
| .query-box::before, |
| .query-box::after { |
| content: 'β¦'; |
| position: absolute; |
| font-size: 10px; |
| color: var(--dendro); |
| opacity: 0.3; |
| top: 8px; |
| } |
| .query-box::before { left: 10px; } |
| .query-box::after { right: 10px; } |
| |
| .query-box:focus-within { |
| border-color: rgba(126,203,106,0.35); |
| box-shadow: 0 0 32px rgba(80,180,60,0.08); |
| } |
| |
| .query-label { |
| padding: 14px 18px 0; |
| font-family: var(--display); |
| font-size: 9px; color: var(--text3); |
| letter-spacing: 0.18em; text-transform: uppercase; |
| } |
| |
| textarea { |
| width: 100%; background: transparent; |
| border: none; outline: none; resize: none; |
| padding: 10px 18px 16px; |
| font-family: var(--serif); font-size: 15px; |
| color: var(--text); line-height: 1.75; |
| min-height: 100px; caret-color: var(--dendro); |
| } |
| |
| textarea::placeholder { color: var(--text3); font-style: italic; } |
| |
| .query-footer { |
| display: flex; align-items: center; |
| justify-content: space-between; |
| padding: 10px 16px; |
| border-top: 1px solid var(--border); |
| background: var(--surface2); |
| } |
| |
| .top-k-wrap { |
| display: flex; align-items: center; |
| gap: 8px; font-family: var(--mono); |
| font-size: 11px; color: var(--text2); |
| } |
| |
| .top-k-wrap select { |
| background: rgba(6,13,8,0.8); |
| border: 1px solid var(--border2); |
| border-radius: 3px; color: var(--text); |
| font-family: var(--mono); font-size: 11px; |
| padding: 3px 8px; cursor: pointer; outline: none; |
| } |
| |
| .send-btn { |
| display: flex; align-items: center; gap: 8px; |
| padding: 9px 22px; |
| background: linear-gradient(135deg, rgba(60,140,50,0.8), rgba(40,100,35,0.9)); |
| border: 1px solid rgba(126,203,106,0.3); |
| border-radius: 3px; |
| color: var(--dendro3); |
| font-family: var(--display); |
| font-size: 11px; font-weight: 500; |
| cursor: pointer; |
| transition: all 0.2s; |
| letter-spacing: 0.1em; |
| box-shadow: 0 2px 20px rgba(60,180,50,0.15); |
| } |
| |
| .send-btn:hover { |
| background: linear-gradient(135deg, rgba(80,160,60,0.9), rgba(55,120,45,0.95)); |
| box-shadow: 0 4px 28px rgba(60,200,60,0.25); |
| border-color: rgba(126,203,106,0.5); |
| transform: translateY(-1px); |
| } |
| |
| .send-btn:active { transform: scale(0.97); } |
| .send-btn:disabled { |
| background: rgba(20,35,20,0.7); |
| color: var(--text3); cursor: not-allowed; |
| transform: none; box-shadow: none; |
| border-color: var(--border); |
| } |
| |
| #response-area { |
| margin-top: 28px; |
| animation: fadeUp 0.5s ease both; |
| display: none; |
| } |
| #response-area.visible { display: block; } |
| |
| .response-card { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 4px; overflow: hidden; |
| backdrop-filter: blur(20px); |
| } |
| |
| .response-header { |
| display: flex; align-items: center; |
| justify-content: space-between; |
| padding: 11px 18px; |
| border-bottom: 1px solid var(--border); |
| background: var(--surface2); |
| } |
| |
| .response-label { |
| font-family: var(--display); font-size: 9px; |
| color: var(--text3); text-transform: uppercase; |
| letter-spacing: 0.14em; |
| display: flex; align-items: center; gap: 8px; |
| } |
| |
| .response-label .dot { |
| width: 6px; height: 6px; border-radius: 50%; |
| background: var(--dendro); |
| box-shadow: 0 0 8px var(--dendro); |
| animation: dendroFlicker 2s ease-in-out infinite; |
| } |
| @keyframes dendroFlicker { |
| 0%,100%{opacity:1;box-shadow:0 0 8px var(--dendro);} |
| 50%{opacity:0.6;box-shadow:0 0 4px var(--dendro);} |
| } |
| |
| .latency-tag { |
| font-family: var(--mono); font-size: 11px; |
| color: var(--text3); padding: 2px 8px; |
| border-radius: 2px; border: 1px solid var(--border); |
| } |
| |
| .response-body { |
| padding: 22px 24px; |
| font-size: 15.5px; line-height: 1.9; |
| color: var(--text); font-family: var(--serif); |
| white-space: normal; word-break: break-word; |
| } |
| .response-body > *:first-child { margin-top: 0; } |
| |
| |
| .response-body h1, |
| .response-body h2 { |
| font-family: var(--display); |
| color: var(--gold); |
| letter-spacing: 0.04em; |
| margin: 1.4em 0 0.4em; |
| font-weight: 500; |
| } |
| .response-body h2 { font-size: 1.15em; border-bottom: 1px solid rgba(212,175,55,0.25); padding-bottom: 0.3em; } |
| .response-body h3 { font-family: var(--serif); color: var(--gold); opacity: 0.85; font-size: 1em; margin: 1.1em 0 0.25em; font-style: italic; } |
| .response-body strong { color: var(--gold); font-weight: 600; } |
| .response-body em { color: var(--text); opacity: 0.8; font-style: italic; } |
| .response-body ul, .response-body ol { |
| padding-left: 1.4em; |
| margin: 0.5em 0 0.8em; |
| } |
| .response-body li { |
| margin-bottom: 0.35em; |
| line-height: 1.75; |
| } |
| .response-body li::marker { color: var(--gold); opacity: 0.7; } |
| .response-body p { margin: 0 0 0.8em; } |
| .response-body p:last-child { margin-bottom: 0; } |
| .response-body hr { |
| border: none; |
| border-top: 1px solid rgba(212,175,55,0.2); |
| margin: 1.2em 0; |
| } |
| .response-body code { |
| font-family: var(--mono); |
| font-size: 0.88em; |
| background: rgba(212,175,55,0.08); |
| border: 1px solid rgba(212,175,55,0.2); |
| border-radius: 3px; |
| padding: 0.1em 0.4em; |
| color: var(--gold); |
| } |
| |
| .thinking { |
| display: flex; align-items: center; gap: 12px; |
| padding: 22px 24px; |
| font-family: var(--serif); font-size: 14px; |
| color: var(--text3); font-style: italic; |
| } |
| |
| .thinking-dots span { |
| display: inline-block; |
| width: 5px; height: 5px; border-radius: 50%; |
| background: var(--dendro); margin: 0 2px; |
| animation: dendroOrb 1.6s ease-in-out infinite; |
| opacity: 0.4; |
| } |
| .thinking-dots span:nth-child(2) { animation-delay: 0.25s; } |
| .thinking-dots span:nth-child(3) { animation-delay: 0.5s; } |
| @keyframes dendroOrb { |
| 0%,100%{opacity:0.3;transform:scale(0.9);} |
| 50%{opacity:1;transform:scale(1.3);box-shadow:0 0 6px var(--dendro);} |
| } |
| |
| .sources-section { |
| border-top: 1px solid var(--border); |
| padding: 12px 22px; |
| background: var(--surface2); |
| } |
| |
| .sources-label { |
| font-family: var(--display); font-size: 9px; |
| color: var(--text3); text-transform: uppercase; |
| letter-spacing: 0.14em; margin-bottom: 8px; |
| } |
| |
| .source-chip { |
| display: inline-flex; align-items: center; gap: 5px; |
| padding: 3px 10px; border-radius: 2px; |
| border: 1px solid rgba(80,180,80,0.18); |
| background: rgba(40,80,30,0.15); |
| font-family: var(--mono); font-size: 11px; |
| color: var(--dendro); margin: 3px 4px 3px 0; |
| } |
| |
| .no-sources { |
| font-family: var(--mono); font-size: 11px; |
| color: var(--text3); font-style: italic; |
| } |
| |
| .error-card { |
| background: rgba(232,120,120,0.05); |
| border: 1px solid rgba(232,120,120,0.18); |
| border-radius: 4px; padding: 14px 18px; |
| font-family: var(--mono); font-size: 12px; |
| color: var(--red); margin-top: 16px; display: none; |
| } |
| .error-card.visible { display: block; } |
| |
| .history-section { |
| margin-top: 32px; padding-bottom: 48px; |
| animation: fadeUp 0.5s ease both; |
| } |
| |
| .history-label { |
| font-family: var(--display); font-size: 9px; |
| color: var(--text3); text-transform: uppercase; |
| letter-spacing: 0.14em; margin-bottom: 12px; |
| } |
| |
| .history-item { |
| background: var(--surface); border: 1px solid var(--border); |
| border-radius: 3px; padding: 11px 16px; |
| margin-bottom: 6px; cursor: pointer; |
| transition: border-color 0.2s, background 0.2s; |
| backdrop-filter: blur(8px); |
| } |
| |
| .history-item:hover { |
| border-color: rgba(80,180,80,0.28); |
| background: rgba(30,60,25,0.3); |
| } |
| |
| .history-q { |
| font-size: 13.5px; color: var(--text2); |
| font-family: var(--serif); |
| white-space: nowrap; overflow: hidden; text-overflow: ellipsis; |
| } |
| |
| .history-meta { |
| font-size: 10px; color: var(--text3); |
| font-family: var(--mono); margin-top: 4px; |
| } |
| |
| footer { |
| position: relative; z-index: 10; |
| text-align: center; padding: 18px; |
| font-family: var(--mono); font-size: 10px; |
| color: var(--text3); letter-spacing: 0.06em; |
| border-top: 1px solid var(--border); |
| backdrop-filter: blur(8px); |
| background: rgba(6,13,8,0.6); |
| margin-top: auto; |
| } |
| |
| footer span { color: var(--dendro); opacity: 0.6; } |
| </style> |
| </head> |
| <body> |
|
|
| <canvas id="bg"></canvas> |
| <div class="atmo atmo-1"></div> |
| <div class="atmo atmo-2"></div> |
| <div class="atmo atmo-3"></div> |
|
|
| <header> |
| <div class="logo"> |
| <div class="logo-mark">αͺ₯</div> |
| <div> |
| <div class="logo-text">Irminsul</div> |
| <div class="logo-sub">Llama 3.1 Β· QLoRA Β· Pinecone RAG</div> |
| </div> |
| </div> |
| <div class="status-pill"> |
| <div class="status-dot" id="status-dot"></div> |
| <span id="status-text">checking...</span> |
| </div> |
| </header> |
|
|
| <main> |
| <div class="hero"> |
| <div class="rune-row"> |
| <span>αͺ₯</span><span>β¦</span><span>αͺ₯</span> |
| </div> |
| <h1>The <em>Akasha</em> Speaks</h1> |
| <p class="hero-sub">All knowledge flows through the tree. Ask of Teyvat's lore, its people, their battles, and the elements that bind this world.</p> |
| <div class="model-tag"> |
| β Dendro-augmented retrieval Β· Genshin Impact corpus |
| </div> |
| </div> |
|
|
| <div class="ornament"> |
| <div class="ornament-line"></div> |
| <div class="ornament-glyph">β¦ αͺ₯ β¦</div> |
| <div class="ornament-line"></div> |
| </div> |
|
|
| <div class="query-box"> |
| <div class="query-label">Inscribe your query</div> |
| <textarea |
| id="query-input" |
| placeholder="What does the Irminsul record of Nahida's imprisonment reveal..." |
| rows="3" |
| ></textarea> |
| <div class="query-footer"> |
| <div class="top-k-wrap"> |
| <span>top_k</span> |
| <select id="top-k"> |
| <option value="1">1</option> |
| <option value="2">2</option> |
| <option value="3" selected>3</option> |
| <option value="5">5</option> |
| </select> |
| <span style="color:var(--text3)">retrieved branches</span> |
| </div> |
| <button class="send-btn" id="send-btn" onclick="submitQuery()"> |
| Query the Tree β |
| </button> |
| </div> |
| </div> |
|
|
| <div class="error-card" id="error-card"></div> |
|
|
| <div id="response-area"> |
| <div class="response-card"> |
| <div class="response-header"> |
| <div class="response-label"> |
| <div class="dot"></div> |
| irminsul responds |
| </div> |
| <div class="latency-tag" id="latency-tag">β</div> |
| </div> |
| <div id="response-body" class="response-body"></div> |
| <div class="sources-section"> |
| <div class="sources-label">branches consulted</div> |
| <div id="sources-list"><span class="no-sources">no records ingested</span></div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="history-section" id="history-section" style="display:none"> |
| <div class="history-label">Recent queries</div> |
| <div id="history-list"></div> |
| </div> |
| </main> |
|
|
| <footer> |
| Irminsul Β· <span>Genshin Impact AI Assistant</span> Β· exp2_lr2e-4_r16 + Groq |
| </footer> |
|
|
| <script> |
| |
| (function() { |
| const canvas = document.getElementById('bg'); |
| const ctx = canvas.getContext('2d'); |
| let W, H, particles = [], wisps = []; |
| |
| function resize() { |
| W = canvas.width = window.innerWidth; |
| H = canvas.height = window.innerHeight; |
| } |
| |
| function rand(a, b) { return a + Math.random() * (b - a); } |
| |
| function initParticles() { |
| particles = []; |
| const n = Math.floor(W * H / 8000); |
| for (let i = 0; i < n; i++) { |
| particles.push({ |
| x: rand(0, W), y: rand(0, H), |
| r: rand(0.8, 2.5), |
| vx: rand(-0.15, 0.15), |
| vy: rand(-0.4, -0.1), |
| alpha: rand(0.1, 0.5), |
| flicker: rand(0.002, 0.006), |
| phase: rand(0, Math.PI * 2), |
| hue: rand(100, 150), |
| }); |
| } |
| } |
| |
| function initWisps() { |
| wisps = []; |
| for (let i = 0; i < 6; i++) { |
| wisps.push({ |
| x: rand(0, W), y: rand(H * 0.3, H * 0.9), |
| r: rand(60, 140), |
| alpha: rand(0.02, 0.06), |
| speed: rand(0.0003, 0.0008), |
| phase: rand(0, Math.PI * 2), |
| hue: rand(110, 145), |
| }); |
| } |
| } |
| |
| function draw(t) { |
| ctx.clearRect(0, 0, W, H); |
| |
| |
| wisps.forEach(w => { |
| const a = w.alpha * (0.6 + 0.4 * Math.sin(t * w.speed * 1000 + w.phase)); |
| const grd = ctx.createRadialGradient(w.x, w.y, 0, w.x, w.y, w.r); |
| grd.addColorStop(0, `hsla(${w.hue},70%,55%,${a})`); |
| grd.addColorStop(1, `hsla(${w.hue},70%,40%,0)`); |
| ctx.beginPath(); |
| ctx.arc(w.x, w.y, w.r, 0, Math.PI * 2); |
| ctx.fillStyle = grd; |
| ctx.fill(); |
| w.x += Math.sin(t * 0.0003 + w.phase) * 0.3; |
| w.y += Math.cos(t * 0.0002 + w.phase) * 0.2; |
| }); |
| |
| |
| particles.forEach(p => { |
| const a = p.alpha * (0.5 + 0.5 * Math.sin(t * p.flicker * 1000 + p.phase)); |
| ctx.beginPath(); |
| ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); |
| ctx.fillStyle = `hsla(${p.hue},80%,70%,${a})`; |
| ctx.fill(); |
| |
| |
| if (p.r > 1.8) { |
| ctx.beginPath(); |
| ctx.arc(p.x, p.y, p.r * 2.5, 0, Math.PI * 2); |
| ctx.fillStyle = `hsla(${p.hue},80%,70%,${a * 0.15})`; |
| ctx.fill(); |
| } |
| |
| p.x += p.vx + Math.sin(t * 0.0005 + p.phase) * 0.2; |
| p.y += p.vy; |
| |
| if (p.y < -10) { p.y = H + 10; p.x = rand(0, W); } |
| if (p.x < -10) p.x = W + 10; |
| if (p.x > W + 10) p.x = -10; |
| }); |
| |
| requestAnimationFrame(draw); |
| } |
| |
| window.addEventListener('resize', () => { resize(); initParticles(); initWisps(); }); |
| resize(); initParticles(); initWisps(); |
| requestAnimationFrame(draw); |
| })(); |
| |
| |
| const API = window.location.origin;; |
| const history = []; |
| |
| async function checkHealth() { |
| const dot = document.getElementById('status-dot'); |
| const txt = document.getElementById('status-text'); |
| dot.className = 'status-dot loading'; |
| txt.textContent = 'connecting...'; |
| try { |
| const r = await fetch(`${API}/health`); |
| const d = await r.json(); |
| if (d.model_loaded) { |
| dot.className = 'status-dot online'; |
| txt.textContent = 'akasha linked'; |
| } else { |
| dot.className = 'status-dot loading'; |
| txt.textContent = 'loading...'; |
| setTimeout(checkHealth, 3000); |
| } |
| } catch { |
| dot.className = 'status-dot error'; |
| txt.textContent = 'offline'; |
| setTimeout(checkHealth, 5000); |
| } |
| } |
| |
| async function submitQuery() { |
| const query = document.getElementById('query-input').value.trim(); |
| if (!query) return; |
| |
| const top_k = parseInt(document.getElementById('top-k').value); |
| const btn = document.getElementById('send-btn'); |
| const respArea = document.getElementById('response-area'); |
| const respBody = document.getElementById('response-body'); |
| const errCard = document.getElementById('error-card'); |
| const latTag = document.getElementById('latency-tag'); |
| const srcList = document.getElementById('sources-list'); |
| |
| errCard.className = 'error-card'; |
| respArea.style.display = 'block'; |
| respArea.className = 'response-area visible'; |
| latTag.textContent = 'β'; |
| srcList.innerHTML = ''; |
| btn.disabled = true; |
| |
| respBody.innerHTML = ` |
| <div class="thinking"> |
| <div class="thinking-dots"><span></span><span></span><span></span></div> |
| searching the branches... |
| </div>`; |
| |
| try { |
| const res = await fetch(`${API}/generate`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ query, top_k }) |
| }); |
| |
| if (!res.ok) { |
| const err = await res.json(); |
| throw new Error(JSON.stringify(err.detail || err)); |
| } |
| |
| const data = await res.json(); |
| respBody.innerHTML = marked.parse(data.answer); |
| |
| const ms = Math.round(data.latency_ms); |
| latTag.textContent = ms >= 1000 ? `${(ms/1000).toFixed(1)}s` : `${ms}ms`; |
| |
| if (data.sources && data.sources.length > 0) { |
| srcList.innerHTML = data.sources.map(s => { |
| const name = s.split(/[\\/]/).pop(); |
| return `<span class="source-chip">β ${name}</span>`; |
| }).join(''); |
| } else { |
| srcList.innerHTML = '<span class="no-sources">no records found β run ingest.py</span>'; |
| } |
| |
| addToHistory(query, ms); |
| |
| } catch (err) { |
| respBody.innerHTML = ''; |
| respArea.style.display = 'none'; |
| errCard.className = 'error-card visible'; |
| errCard.textContent = `Error: ${err.message}`; |
| } |
| |
| btn.disabled = false; |
| } |
| |
| function addToHistory(query, latency_ms) { |
| history.unshift({ query, latency_ms, time: new Date().toLocaleTimeString() }); |
| if (history.length > 5) history.pop(); |
| |
| document.getElementById('history-section').style.display = 'block'; |
| document.getElementById('history-list').innerHTML = history.map((h, i) => ` |
| <div class="history-item" onclick="rerun(${i})"> |
| <div class="history-q">${h.query}</div> |
| <div class="history-meta">${h.time} Β· ${(h.latency_ms / 1000).toFixed(1)}s</div> |
| </div> |
| `).join(''); |
| } |
| |
| function rerun(i) { |
| document.getElementById('query-input').value = history[i].query; |
| submitQuery(); |
| } |
| |
| document.getElementById('query-input').addEventListener('keydown', e => { |
| if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { |
| e.preventDefault(); |
| submitQuery(); |
| } |
| }); |
| |
| checkHealth(); |
| </script> |
| </body> |
| </html> |