finnie / presentation.html
Vishnu Rama
Add technical design document and presentation
d8a60cc
Raw
History Blame Contribute Delete
24.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Finnie β€” AI Finance Assistant</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',system-ui,sans-serif;background:#07090f;color:#e8eaf0;height:100vh;overflow:hidden;user-select:none}
#deck{width:100vw;height:100vh;position:relative}
.slide{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:48px 72px;opacity:0;pointer-events:none;transition:opacity .3s ease}
.slide.active{opacity:1;pointer-events:all}
.label{font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;color:#4ade80;margin-bottom:10px;align-self:flex-start}
h1{font-size:3.2rem;font-weight:800;color:#fff;line-height:1.1}
h2{font-size:1.9rem;font-weight:700;color:#fff;align-self:flex-start;margin-bottom:22px}
p{font-size:.98rem;line-height:1.65;color:#9aa3b8}
.badge{display:inline-block;background:#111827;border:1px solid #1e3050;border-radius:6px;padding:4px 13px;font-size:.8rem;color:#4ade80;margin:3px}
.accent{width:72px;height:4px;background:linear-gradient(90deg,#4ade80,#22d3ee);border-radius:2px;margin:20px 0}
/* ── Slide 2: tabs ── */
.tab-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;width:100%;max-width:860px;margin-bottom:22px}
.tab-card{background:#0d1526;border:1px solid #1e2d50;border-radius:14px;padding:22px 20px}
.tab-card h3{font-size:1rem;font-weight:700;color:#4ade80;margin-bottom:8px}
.tab-card p{font-size:.87rem;color:#8892a4;line-height:1.55}
.agent-row{display:flex;flex-wrap:wrap;gap:8px;width:100%;max-width:860px}
.agent-pill{background:#111827;border:1px solid #1e3050;border-radius:20px;padding:5px 14px;font-size:.82rem;color:#c8ccd8}
.agent-pill span{color:#4ade80;font-weight:600}
/* ── Slide 3: arch ── */
.arch{display:flex;flex-direction:column;align-items:center;gap:0;width:100%;max-width:860px}
.arch-row{display:flex;align-items:stretch;justify-content:center;width:100%;gap:8px}
.arch-box{background:#0d1526;border:1px solid #1e2d50;border-radius:10px;padding:12px 18px;text-align:center;flex:1}
.arch-box.main{border-color:#4ade80;background:#081a0e}
.arch-box.llm{border-color:#7dd3fc;background:#071624}
.arch-box h4{font-size:.88rem;font-weight:700;color:#fff;margin-bottom:3px}
.arch-box p{font-size:.76rem;color:#6b7a99}
.arch-arrow{display:flex;align-items:center;justify-content:center;height:28px;color:#4ade80;font-size:1.3rem}
.arch-label{font-size:.7rem;letter-spacing:.1em;text-transform:uppercase;color:#4ade80;margin-bottom:5px;align-self:flex-start}
/* ── Slide 4: graph ── */
.graph-wrap{width:100%;max-width:880px;display:flex;flex-direction:column;align-items:center}
/* ── Slide 5 ── */
.demo-list{width:100%;max-width:760px}
.demo-item{display:flex;align-items:flex-start;gap:14px;padding:11px 0;border-bottom:1px solid #111827}
.demo-item:last-child{border-bottom:none}
.demo-num{background:#4ade80;color:#07090f;font-weight:800;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:.8rem;margin-top:1px}
.demo-tab{font-size:.72rem;background:#1a2540;color:#7dd3fc;border-radius:4px;padding:2px 7px;font-weight:700;margin-right:7px}
.demo-item p{font-size:.93rem;color:#9aa3b8}
.demo-item strong{color:#fff}
.agents-badge{font-size:.68rem;background:#081a0e;border:1px solid #1e3050;border-radius:10px;padding:2px 7px;color:#4ade80;margin-left:6px;white-space:nowrap}
/* ── Controls ── */
#controls{position:fixed;bottom:28px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:18px;background:#0d1526cc;backdrop-filter:blur(8px);border:1px solid #1e293b;border-radius:40px;padding:9px 22px}
button{background:none;border:none;color:#4ade80;font-size:1.25rem;cursor:pointer;padding:3px 7px;border-radius:6px;transition:background .2s;line-height:1}
button:hover{background:#1a2540}
button:disabled{color:#2a3a60;cursor:default}
button:disabled:hover{background:none}
#counter{font-size:.82rem;color:#8892a4;min-width:48px;text-align:center}
#progress{position:fixed;top:0;left:0;height:3px;background:#4ade80;transition:width .3s ease}
</style>
</head>
<body>
<div id="progress"></div>
<div id="deck">
<!-- ══════════════════════════════════════════════════════
SLIDE 1 Β· TITLE
══════════════════════════════════════════════════════ -->
<div class="slide active">
<div style="max-width:820px;width:100%">
<div class="label">Interview Kickstart Β· Applied Agentic AI for SWEs Β· Capstone</div>
<h1>Finnie</h1>
<h1 style="color:#4ade80">AI Finance Education<br/>Assistant</h1>
<div class="accent"></div>
<p style="font-size:1.1rem;max-width:580px;margin-bottom:24px">
A conversational multi-agent system with hybrid orchestration β€” routes every query
to two or more specialized agents, then synthesizes a single coherent answer.
</p>
<div>
<span class="badge">LangGraph</span>
<span class="badge">LangChain</span>
<span class="badge">RAG</span>
<span class="badge">Pydantic</span>
<span class="badge">yfinance</span>
<span class="badge">Alpha Vantage</span>
<span class="badge">Streamlit</span>
<span class="badge">Hugging Face Spaces</span>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════════
SLIDE 2 Β· WHAT IT DOES
══════════════════════════════════════════════════════ -->
<div class="slide">
<div style="width:100%;max-width:860px">
<div class="label">Product</div>
<h2>Three Tabs, Six Agents</h2>
<div class="tab-grid">
<div class="tab-card">
<h3>πŸ’¬ Chat</h3>
<p>Conversational interface with memory across turns. Always routes to β‰₯ 2 agents. Agent badges show exactly which specialists answered.</p>
</div>
<div class="tab-card">
<h3>πŸ“Š Portfolio</h3>
<p>Paste holdings in any format. LLM parses it, fetches live prices, scores diversification, shows sector breakdown with charts.</p>
</div>
<div class="tab-card">
<h3>πŸ“ˆ Market</h3>
<p>Real-time quote for any company β€” ticker or name. 52-week range gauge, 30-day price history chart, AI narrative analysis.</p>
</div>
</div>
<p style="font-size:.82rem;color:#4ade80;text-transform:uppercase;letter-spacing:.12em;margin-bottom:10px">Six Specialized Agents</p>
<div class="agent-row">
<div class="agent-pill"><span>QA</span> Financial knowledge RAG</div>
<div class="agent-pill"><span>Portfolio</span> Live analysis + scoring</div>
<div class="agent-pill"><span>Market</span> Real-time data + 52w/P/E</div>
<div class="agent-pill"><span>Goal</span> Savings plan math</div>
<div class="agent-pill"><span>News</span> Headlines + sentiment</div>
<div class="agent-pill"><span>Tax</span> Capital gains, IRA, 401k</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════════
SLIDE 3 Β· SYSTEM ARCHITECTURE
══════════════════════════════════════════════════════ -->
<div class="slide">
<div style="width:100%;max-width:860px">
<div class="label">Architecture</div>
<h2>System Design</h2>
<div class="arch">
<!-- User -->
<div class="arch-row">
<div class="arch-box" style="max-width:280px;border-color:#4ade80;background:#081a0e">
<h4 style="color:#4ade80">πŸ‘€ User</h4>
<p>Browser β€” Hugging Face Spaces</p>
</div>
</div>
<div class="arch-arrow">↓</div>
<!-- Streamlit -->
<div class="arch-row">
<div class="arch-box main" style="border-color:#22d3ee;background:#061820">
<h4 style="color:#22d3ee">Streamlit Web App</h4>
<p>Chat tab Β· Portfolio tab Β· Market tab</p>
</div>
</div>
<div class="arch-arrow">↓</div>
<!-- Router -->
<div class="arch-row">
<div class="arch-box" style="border-color:#fbbf24;background:#0f0e04;max-width:680px">
<h4 style="color:#fbbf24">chat_invoke() β€” Hybrid Router</h4>
<p>19 keyword patterns β†’ Smart Fan-Out (parallel β‰₯ 2 agents)&nbsp;&nbsp;Β·&nbsp;&nbsp;context-dependent keywords β†’ ReAct (sequential)</p>
</div>
</div>
<div class="arch-arrow" style="gap:60px;display:flex;font-size:.9rem;color:#fbbf24">
<span>↙ fan-out</span><span>β†˜ sequential</span>
</div>
<!-- Two LangGraph boxes -->
<div class="arch-row">
<div class="arch-box main">
<h4 style="color:#4ade80">Smart Fan-Out Graph</h4>
<p>Param Extractor β†’ Smart Fan-Out Node β†’ Tools (parallel) β†’ Synth</p>
</div>
<div class="arch-box" style="border-color:#7dd3fc;background:#071624">
<h4 style="color:#7dd3fc">ReAct Graph</h4>
<p>Param Extractor β†’ Agent Node ⟷ Tool Node Β· MemorySaver per session</p>
</div>
</div>
<div class="arch-arrow">↓</div>
<!-- Agents -->
<div class="arch-row">
<div class="arch-box"><h4>QA</h4><p>RAG</p></div>
<div class="arch-box"><h4>Portfolio</h4><p>yfinance</p></div>
<div class="arch-box"><h4>Market</h4><p>Alpha Vantage</p></div>
<div class="arch-box"><h4>Goal</h4><p>Math</p></div>
<div class="arch-box"><h4>News</h4><p>RSS feeds</p></div>
<div class="arch-box"><h4>Tax</h4><p>Rules</p></div>
</div>
<div class="arch-arrow">↓</div>
<!-- External -->
<div class="arch-row">
<div class="arch-box llm"><h4 style="color:#7dd3fc">Claude (Anthropic)</h4><p>LLM Β· structured output Β· tool routing</p></div>
<div class="arch-box" style="border-color:#a78bfa;background:#0e0a1e"><h4 style="color:#a78bfa">Vector Store</h4><p>RAG knowledge base</p></div>
<div class="arch-box"><h4>Market APIs</h4><p>yfinance Β· Alpha Vantage</p></div>
</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════════
SLIDE 4 Β· BOTH LANGGRAPH GRAPHS
══════════════════════════════════════════════════════ -->
<div class="slide">
<div class="graph-wrap">
<div class="label">Workflow</div>
<h2>Two LangGraph Paths Β· Hybrid Routing</h2>
<svg viewBox="0 0 880 308" style="width:100%;max-width:880px;overflow:visible" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="ag" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#4ade80"/></marker>
<marker id="ap" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#c084fc"/></marker>
<marker id="ar" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#f87171"/></marker>
<marker id="ab" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0,8 3,0 6" fill="#7dd3fc"/></marker>
</defs>
<!-- Center divider -->
<line x1="440" y1="0" x2="440" y2="308" stroke="#1e2d50" stroke-width="1" stroke-dasharray="4 4"/>
<!-- ════════════ LEFT: SMART FAN-OUT ════════════ -->
<text x="213" y="13" text-anchor="middle" fill="#4ade80" font-size="10.5" font-weight="700" letter-spacing=".08em">SMART FAN-OUT Β· DEFAULT PATH</text>
<!-- START -->
<circle cx="213" cy="36" r="17" fill="#081a0e" stroke="#4ade80" stroke-width="2"/>
<text x="213" y="39" text-anchor="middle" fill="#4ade80" font-size="9" font-weight="700">START</text>
<!-- Param Extractor -->
<rect x="103" y="59" width="220" height="40" rx="7" fill="#0d1526" stroke="#4ade80" stroke-width="1.5"/>
<text x="213" y="77" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">Param Extractor</text>
<text x="213" y="92" text-anchor="middle" fill="#6b7a99" font-size="9">goal Β· savings Β· risk Β· portfolio Β· age</text>
<!-- Smart Fan-Out Node -->
<rect x="78" y="112" width="270" height="42" rx="7" fill="#081a0e" stroke="#4ade80" stroke-width="2"/>
<text x="213" y="131" text-anchor="middle" fill="#4ade80" font-size="11" font-weight="700">Smart Fan-Out Node</text>
<text x="213" y="147" text-anchor="middle" fill="#6b7a99" font-size="9">LLM selects β‰₯ 2 tools via 9 routing rules</text>
<!-- parallel label -->
<text x="213" y="166" text-anchor="middle" fill="#c084fc" font-size="8" font-style="italic">── parallel execution ──</text>
<!-- Tool A / B / C -->
<rect x="18" y="170" width="114" height="36" rx="6" fill="#0d1526" stroke="#c084fc" stroke-width="1.3"/>
<text x="75" y="186" text-anchor="middle" fill="#fff" font-size="10" font-weight="600">Tool A</text>
<text x="75" y="199" text-anchor="middle" fill="#6b7a99" font-size="8.5">e.g. analyze_portfolio</text>
<rect x="149" y="170" width="126" height="36" rx="6" fill="#0d1526" stroke="#c084fc" stroke-width="1.3"/>
<text x="212" y="186" text-anchor="middle" fill="#fff" font-size="10" font-weight="600">Tool B</text>
<text x="212" y="199" text-anchor="middle" fill="#6b7a99" font-size="8.5">e.g. plan_financial_goal</text>
<rect x="292" y="170" width="114" height="36" rx="6" fill="#0d1526" stroke="#c084fc" stroke-width="1.3"/>
<text x="349" y="186" text-anchor="middle" fill="#fff" font-size="10" font-weight="600">Tool C</text>
<text x="349" y="199" text-anchor="middle" fill="#6b7a99" font-size="8.5">e.g. get_tax_education</text>
<!-- Synth Node -->
<rect x="100" y="222" width="226" height="40" rx="7" fill="#071624" stroke="#7dd3fc" stroke-width="1.5"/>
<text x="213" y="240" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">Synth Node</text>
<text x="213" y="255" text-anchor="middle" fill="#6b7a99" font-size="9">LLM merges all results β†’ one answer</text>
<!-- END -->
<circle cx="213" cy="285" r="15" fill="#1a0a0a" stroke="#f87171" stroke-width="2"/>
<text x="213" y="289" text-anchor="middle" fill="#f87171" font-size="9" font-weight="700">END</text>
<!-- LEFT arrows -->
<line x1="213" y1="53" x2="213" y2="59" stroke="#4ade80" stroke-width="1.8" marker-end="url(#ag)"/>
<line x1="213" y1="99" x2="213" y2="112" stroke="#4ade80" stroke-width="1.8" marker-end="url(#ag)"/>
<!-- fan-out (SmartFanOut β†’ tools) -->
<line x1="140" y1="154" x2="86" y2="170" stroke="#c084fc" stroke-width="1.5" marker-end="url(#ap)"/>
<line x1="213" y1="154" x2="213" y2="170" stroke="#c084fc" stroke-width="1.5" marker-end="url(#ap)"/>
<line x1="283" y1="154" x2="337" y2="170" stroke="#c084fc" stroke-width="1.5" marker-end="url(#ap)"/>
<!-- fan-in (tools β†’ Synth) -->
<line x1="86" y1="206" x2="148" y2="222" stroke="#c084fc" stroke-width="1.5" marker-end="url(#ap)"/>
<line x1="213" y1="206" x2="213" y2="222" stroke="#c084fc" stroke-width="1.5" marker-end="url(#ap)"/>
<line x1="337" y1="206" x2="277" y2="222" stroke="#c084fc" stroke-width="1.5" marker-end="url(#ap)"/>
<!-- Synth β†’ END -->
<line x1="213" y1="262" x2="213" y2="270" stroke="#f87171" stroke-width="1.8" marker-end="url(#ar)"/>
<!-- ════════════ RIGHT: REACT ════════════ -->
<text x="660" y="13" text-anchor="middle" fill="#7dd3fc" font-size="10.5" font-weight="700" letter-spacing=".08em">REACT SEQUENTIAL Β· CONTEXT PATH</text>
<text x="660" y="25" text-anchor="middle" fill="#3d5070" font-size="8.5">P/E Β· rate hike Β· dividend Β· 52-week Β· drops Β· allocation Β· compare</text>
<!-- START -->
<circle cx="660" cy="44" r="17" fill="#081a0e" stroke="#4ade80" stroke-width="2"/>
<text x="660" y="47" text-anchor="middle" fill="#4ade80" font-size="9" font-weight="700">START</text>
<!-- Param Extractor -->
<rect x="550" y="67" width="220" height="40" rx="7" fill="#0d1526" stroke="#4ade80" stroke-width="1.5"/>
<text x="660" y="85" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">Param Extractor</text>
<text x="660" y="100" text-anchor="middle" fill="#6b7a99" font-size="9">goal Β· savings Β· risk Β· portfolio Β· age</text>
<!-- Agent Node -->
<rect x="545" y="123" width="230" height="48" rx="7" fill="#071624" stroke="#7dd3fc" stroke-width="2"/>
<text x="660" y="143" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">Agent Node</text>
<text x="660" y="159" text-anchor="middle" fill="#6b7a99" font-size="9">Claude LLM Β· bound tools Β· must call β‰₯ 2</text>
<!-- Tool Node -->
<rect x="545" y="205" width="230" height="44" rx="7" fill="#120a20" stroke="#c084fc" stroke-width="1.5"/>
<text x="660" y="226" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">Tool Node</text>
<text x="660" y="241" text-anchor="middle" fill="#6b7a99" font-size="9">executes chosen agent Β· result feeds back</text>
<!-- END (right of Agent) -->
<circle cx="834" cy="147" r="17" fill="#1a0a0a" stroke="#f87171" stroke-width="2"/>
<text x="834" y="151" text-anchor="middle" fill="#f87171" font-size="9" font-weight="700">END</text>
<text x="834" y="172" text-anchor="middle" fill="#f87171" font-size="7.5">final answer</text>
<!-- RIGHT arrows -->
<line x1="660" y1="61" x2="660" y2="67" stroke="#4ade80" stroke-width="1.8" marker-end="url(#ag)"/>
<line x1="660" y1="107" x2="660" y2="123" stroke="#4ade80" stroke-width="1.8" marker-end="url(#ag)"/>
<!-- Agent β†’ END (no tool call) red -->
<line x1="775" y1="147" x2="815" y2="147" stroke="#f87171" stroke-width="1.8" marker-end="url(#ar)"/>
<rect x="769" y="130" width="66" height="13" rx="3" fill="#07090f"/>
<text x="802" y="141" text-anchor="middle" fill="#f87171" font-size="8">no tool call</text>
<!-- Agent β†’ Tool (tool call) purple down -->
<line x1="660" y1="171" x2="660" y2="205" stroke="#c084fc" stroke-width="1.8" marker-end="url(#ap)"/>
<rect x="665" y="181" width="52" height="13" rx="3" fill="#07090f"/>
<text x="691" y="192" text-anchor="middle" fill="#c084fc" font-size="8">tool call</text>
<!-- Tool β†’ Agent loop (left side) -->
<path d="M 545 227 Q 492 227 492 147 L 543 147" stroke="#c084fc" stroke-width="1.8" fill="none" marker-end="url(#ap)"/>
<rect x="459" y="186" width="46" height="24" rx="3" fill="#07090f"/>
<text x="482" y="197" text-anchor="middle" fill="#c084fc" font-size="7.5">result</text>
<text x="482" y="207" text-anchor="middle" fill="#c084fc" font-size="7.5">β†’ loop</text>
</svg>
<div style="text-align:center;margin-top:10px">
<span class="badge">chat_invoke()</span>
<span style="color:#9aa3b8;font-size:.8rem;margin:0 8px">routes via 19 keyword patterns Β· both graphs share</span>
<span class="badge">MemorySaver</span>
<span style="color:#9aa3b8;font-size:.8rem;margin:0 8px">state Β· portfolio Β· age across turns</span>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════════
SLIDE 5 Β· DEMO + THANK YOU
══════════════════════════════════════════════════════ -->
<div class="slide">
<div style="width:100%;max-width:820px;display:grid;grid-template-columns:1fr 1fr;gap:48px;align-items:start">
<div>
<div class="label">Live Demo</div>
<h2 style="font-size:1.5rem;margin-bottom:16px">Demo Flow</h2>
<div class="demo-list">
<div class="demo-item">
<div class="demo-num">1</div>
<div><p><span class="demo-tab">Chat</span><strong>General Q&amp;A</strong><span class="agents-badge">QA + Goal</span><br/>"What is compound interest?" β€” RAG + educational context</p></div>
</div>
<div class="demo-item">
<div class="demo-num">2</div>
<div><p><span class="demo-tab">Chat</span><strong>Retirement goal β€” aggressive risk</strong><span class="agents-badge">Goal + Tax</span><br/>"I'm 50, have $600K, want $3M in 20 years β€” am I on track?"</p></div>
</div>
<div class="demo-item">
<div class="demo-num">3</div>
<div><p><span class="demo-tab">Chat</span><strong>Portfolio + market scenario</strong><span class="agents-badge">Portfolio + Market</span><br/>"AAPL:100, NVDA:50, MSFT:200 β€” if Nvidia drops 15%, how am I affected?"</p></div>
</div>
<div class="demo-item">
<div class="demo-num">4</div>
<div><p><span class="demo-tab">Portfolio</span><strong>Live diversification analysis</strong><br/>Paste any format β€” sector charts, diversification score, live prices</p></div>
</div>
<div class="demo-item">
<div class="demo-num">5</div>
<div><p><span class="demo-tab">Chat</span><strong>401k projection mode</strong><span class="agents-badge">Goal + Tax</span><br/>"I'm 25, $7K/yr 401k + $11K employer match. How much by 2050?"</p></div>
</div>
</div>
</div>
<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;gap:16px">
<div style="font-size:3rem">πŸ’š</div>
<h1 style="font-size:2.4rem">Thank You</h1>
<div class="accent" style="align-self:center"></div>
<p style="font-size:.95rem;max-width:300px">Built end-to-end for the Applied Agentic AI for SWEs capstone at Interview Kickstart</p>
<p style="color:#4ade80;font-size:1.1rem;font-weight:700;margin-top:8px">Questions?</p>
<div style="margin-top:12px">
<span class="badge">LangGraph</span>
<span class="badge">Claude</span>
<span class="badge">RAG</span>
<span class="badge">Streamlit</span>
</div>
</div>
</div>
</div>
</div><!-- /deck -->
<!-- Controls -->
<div id="controls">
<button id="prev" onclick="go(-1)" disabled>&#8592;</button>
<span id="counter">1 / 5</span>
<button id="next" onclick="go(1)">&#8594;</button>
</div>
<script>
const slides = document.querySelectorAll('.slide');
const total = slides.length;
let cur = 0;
function go(d) {
slides[cur].classList.remove('active');
cur = Math.max(0, Math.min(total - 1, cur + d));
slides[cur].classList.add('active');
document.getElementById('counter').textContent = `${cur + 1} / ${total}`;
document.getElementById('prev').disabled = cur === 0;
document.getElementById('next').disabled = cur === total - 1;
document.getElementById('progress').style.width = `${((cur + 1) / total) * 100}%`;
}
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight' || e.key === ' ') go(1);
if (e.key === 'ArrowLeft') go(-1);
});
let tx = 0;
document.addEventListener('touchstart', e => { tx = e.touches[0].clientX; });
document.addEventListener('touchend', e => { const d = e.changedTouches[0].clientX - tx; if (Math.abs(d) > 50) go(d < 0 ? 1 : -1); });
document.getElementById('progress').style.width = `${(1/total)*100}%`;
</script>
</body>
</html>