accompy / static /index.html
jasonmdong's picture
Sync from GitHub via hub-sync
c78e8af verified
Raw
History Blame Contribute Delete
20.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NotePilot</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<!-- ── Score list ── -->
<div id="list-screen" class="screen active">
<header id="app-header">
<button class="brand-home" type="button" onclick="goHome()" aria-label="Return to home page">
<svg class="brand-mark" viewBox="0 0 64 64" aria-hidden="true" focusable="false">
<circle class="brand-mark-ring" cx="25" cy="38" r="18"></circle>
<path class="brand-mark-note" d="M30 38.5a11 11 0 1 1-6.5-10V10.5c0-2 2.3-3.1 3.8-1.8l17 13.7c5 4 5.7 11.3 1.4 16.2l-2.5 2.8c1.2-5.6-.7-9.4-5.6-12.4l-8.1-5v14.5z"></path>
<path class="brand-mark-arrow" d="M13.5 39.3l20-8.3-8.3 20-3.9-8-7.8-3.7z"></path>
</svg>
<span class="brand-word">Note<span>Pilot</span></span>
</button>
<div class="top-nav-actions">
<div id="nav-auth-user" class="nav-auth-user" style="display:none">
<span id="nav-auth-username"></span>
<button class="btn btn-ghost nav-logout-btn" type="button" onclick="signOut()">Log out</button>
</div>
<button id="theme-toggle" class="theme-toggle" data-theme-toggle onclick="toggleTheme()" aria-label="Toggle light and dark mode">Light</button>
</div>
</header>
<div id="auth-panel" class="auth-panel">
<div id="auth-logged-out" class="auth-card">
<h3>Sign in</h3>
<p class="auth-copy">Create an account or sign in to keep your own piece library.</p>
<div class="auth-form">
<button id="google-signin-btn" class="btn google-signin-btn" type="button" onclick="signInWithGoogle()">
<span aria-hidden="true">G</span>
Continue with Google
</button>
<div id="google-auth-divider" class="auth-divider"><span>or</span></div>
<input id="auth-email" type="text" placeholder="Username">
<input id="auth-password" type="password" placeholder="Password">
<div class="auth-actions">
<button class="btn btn-primary" type="button" onclick="signIn()">Log in</button>
<button class="btn btn-ghost" type="button" onclick="signUp()">Sign up</button>
<button class="btn btn-ghost guest-signin-btn" type="button" onclick="continueAsGuest()">Continue as Guest</button>
</div>
<div id="auth-status" class="auth-status">Loading sign-in…</div>
</div>
</div>
</div>
<div id="guest-banner" class="guest-banner" style="display:none">
<div>
<strong>Guest Mode</strong>
<span>Changes are saved only on this device.</span>
</div>
<div class="guest-banner-actions">
<button class="btn btn-ghost" type="button" onclick="resetGuestDemo()">Reset Demo</button>
<button class="btn btn-primary" type="button" onclick="showCreateAccountPrompt('Create a free account to save your own pieces and progress.')">Create Account</button>
</div>
</div>
<div class="library-header">
<div>
<h2 id="library-title">Your Library</h2>
<p id="library-subtitle">Choose a piece, set your part, and let NotePilot follow your tempo.</p>
</div>
<button id="add-piece-btn" class="btn btn-primary" onclick="openAddModal()">+ Add piece</button>
</div>
<div class="score-grid-toolbar">
<label for="score-grid-columns">Preview columns</label>
<select id="score-grid-columns" class="instr-select" onchange="setScoreGridColumns(this.value)">
<option value="2">2</option>
<option value="3" selected>3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
</div>
<div id="score-grid" class="score-grid"></div>
</div>
<!-- ── Add piece modal ── -->
<div id="add-modal" class="modal-backdrop" style="display:none" onclick="closeAddModal(event)">
<div class="modal">
<div class="modal-header">
<h3>Add a piece</h3>
<button class="modal-close" onclick="closeAddModal()">βœ•</button>
</div>
<div class="modal-body">
<div class="modal-section">
<h4>Built-in library</h4>
<p class="modal-copy">Search the music21 corpus and add a score directly.</p>
</div>
<div class="search-row">
<input id="corpus-search" type="text" placeholder="Search by composer or piece…" oninput="onSearchInput()" autocomplete="off">
<span id="search-spinner" style="display:none">⏳</span>
</div>
<div id="search-results" class="search-results">
<p class="search-hint">Type to search the built-in music library (535 pieces).</p>
</div>
<div class="upload-divider"></div>
<div class="modal-section">
<h4>Import PDF, images, or MusicXML</h4>
<p class="modal-copy">Upload one PDF, a set of page screenshots, or a MusicXML/MuseScore file. PDF/images go through Audiveris first; MusicXML and MuseScore files import directly.</p>
</div>
<div class="upload-form">
<input id="import-name" type="text" placeholder="Piece name (optional)">
<input id="import-files" class="file-input-hidden" type="file" accept=".pdf,.png,.jpg,.jpeg,.xml,.mxl,.musicxml,.mscx,.mscz,image/png,image/jpeg,application/pdf,text/xml,application/xml" multiple>
<div id="import-dropzone" class="file-dropzone" role="button" tabindex="0" aria-label="Choose files or drag files here">
<div class="file-dropzone-title">Choose files</div>
<div class="file-dropzone-copy">or drag PDF, image, MusicXML, or MuseScore files here</div>
<div id="import-file-label" class="file-dropzone-selection">No files selected</div>
</div>
<div class="upload-actions">
<button id="import-btn" type="button" class="btn btn-primary" onclick="importScoreFiles()">Import</button>
<span id="import-status" class="import-status">No files selected.</span>
</div>
</div>
</div>
</div>
</div>
<!-- ── Confirm dialog ── -->
<div id="confirm-modal" class="modal-backdrop" style="display:none">
<div class="modal confirm-modal">
<div class="modal-header">
<h3 id="confirm-modal-title">Are you sure?</h3>
</div>
<div class="modal-body">
<p id="confirm-modal-message" class="confirm-modal-message"></p>
<div class="confirm-modal-actions">
<button id="confirm-modal-cancel" class="btn btn-ghost" type="button">Cancel</button>
<button id="confirm-modal-ok" class="btn btn-danger" type="button">Delete</button>
</div>
</div>
</div>
</div>
<!-- ── Play screen ── -->
<div id="play-screen" class="screen">
<div class="play-shell">
<aside class="play-sidebar">
<div class="play-sidebar-header">
<div class="play-sidebar-copy">
<h3>Pieces</h3>
<p>Switch pieces without returning to the gallery.</p>
</div>
<button
id="play-sidebar-toggle"
class="play-sidebar-toggle"
type="button"
onclick="togglePlaySidebar()"
aria-label="Collapse piece sidebar">☰</button>
</div>
<div id="play-piece-list" class="play-piece-list"></div>
<div id="play-guest-banner" class="play-guest-banner" style="display:none">
<strong>Guest Mode</strong>
<span>Changes are saved only on this device.</span>
<div>
<button class="btn btn-ghost" type="button" onclick="resetGuestDemo()">Reset Demo</button>
<button class="btn btn-primary" type="button" onclick="showCreateAccountPrompt('Create a free account to save your own pieces and progress.')">Create Account</button>
</div>
</div>
<div class="play-sidebar-footer">
<button class="sidebar-brand-home" type="button" onclick="goHome()" aria-label="Return to home page">
<span class="sidebar-brand-dot" aria-hidden="true">N</span>
<span class="sidebar-brand-word">Note<span>Pilot</span></span>
</button>
<div class="sidebar-footer-actions">
<button class="theme-toggle sidebar-theme-toggle" data-theme-toggle onclick="toggleTheme()" aria-label="Toggle light and dark mode">Light</button>
<div class="sidebar-account-menu">
<button
id="sidebar-account-btn"
class="sidebar-account-btn"
type="button"
onclick="toggleSidebarAccountMenu()"
aria-label="Open account menu"
aria-expanded="false">?</button>
<div id="sidebar-account-popover" class="sidebar-account-popover" style="display:none">
<span class="sidebar-account-eyebrow">Profile</span>
<strong id="sidebar-account-name">Not signed in</strong>
<span id="sidebar-account-detail">Sign in to save your library.</span>
<button id="sidebar-account-action" class="btn btn-ghost" type="button" onclick="sidebarAccountAction()">Sign in</button>
</div>
</div>
</div>
</div>
</aside>
<div
class="layout-splitter shell-splitter"
id="shell-splitter"
role="separator"
aria-label="Resize pieces sidebar"
aria-orientation="vertical"
aria-valuemin="180"
aria-valuemax="420"
aria-valuenow="260"
tabindex="0">
<span></span>
</div>
<div class="play-content">
<div class="progress-bar"><div id="progress-fill" class="progress-fill" style="width:0%"></div></div>
<div class="practice-area">
<div class="practice-workspace" id="practice-workspace">
<div class="sheet-section">
<div class="sheet-header">
<div class="sheet-title-row">
<h4>Sheet music</h4>
<span id="sheet-fingering-status" class="sheet-status-badge" style="display:none"></span>
</div>
<div class="sheet-toolbar">
<button id="sheet-generate-fingering-btn" class="sheet-action-btn" onclick="generateSheetFingering()" style="display:none">Generate fingering</button>
<button id="sheet-toggle-fingering-btn" class="sheet-action-btn" onclick="toggleSheetFingering()" style="display:none">Show fingering</button>
<button class="sheet-tool-btn" onclick="sheetZoom(-0.1)" title="Zoom out" aria-label="Zoom out">βˆ’</button>
<span id="sheet-zoom-label" class="sheet-zoom-label">100%</span>
<button class="sheet-tool-btn" onclick="sheetZoom(0.1)" title="Zoom in" aria-label="Zoom in">+</button>
<button class="sheet-tool-btn" onclick="sheetResetView()" title="Reset view" aria-label="Reset view">⟲</button>
<button class="sheet-tool-btn" onclick="sheetRotate()" title="Rotate 90Β°" aria-label="Rotate">↻</button>
<button class="sheet-tool-btn" onclick="sheetDownload()" title="Download source" aria-label="Download">⬇</button>
</div>
</div>
<div id="sheet-fingering-progress" class="sheet-fingering-progress" style="display:none">
<div class="sheet-fingering-progress-meta">
<span id="sheet-fingering-progress-message">Generating fingering</span>
<span id="sheet-fingering-progress-value">0%</span>
</div>
<div class="sheet-fingering-progress-bar" aria-hidden="true">
<div id="sheet-fingering-progress-fill" class="sheet-fingering-progress-fill"></div>
</div>
</div>
<div class="sheet-viewer" id="sheet-viewer">
<div class="sheet-content-wrap" id="sheet-content-wrap">
<iframe id="sheet-frame" style="display:none"></iframe>
<div id="sheet-musicxml-fallback" class="sheet-musicxml-fallback" style="display:none"></div>
</div>
<div id="sheet-placeholder" class="sheet-placeholder">
No sheet music available for this score.<br>
Re-run <code>convert_score.py</code> with verovio installed to generate it.
</div>
</div>
</div>
<div
class="practice-splitter"
id="practice-splitter"
role="separator"
aria-label="Resize sheet music and keyboard visualizer"
aria-orientation="horizontal"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="50"
tabindex="0">
<span></span>
</div>
<div class="keyboard-section" id="keyboard-section">
<div class="keyboard-heading">
<div class="keyboard-heading-main">
<h4>Keyboard visualizer</h4>
<div class="help-tip" tabindex="0" aria-label="Keyboard layout help">
?
<div class="help-tip-popup">A S D J K L ; play C D E F G A B. W E I O P play the black keys.</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:.6rem">
<span id="midi-status" class="midi-badge midi-badge-disconnected" title="MIDI piano status">
<span class="midi-badge-dot"></span>
<span id="midi-status-text">No piano detected</span>
</span>
<div class="keyboard-view-toggle">
<button class="keyboard-view-btn" id="keyboard-view-full" onclick="setKeyboardLayoutMode('full')">Full</button>
<button class="keyboard-view-btn" id="keyboard-view-mini" onclick="setKeyboardLayoutMode('mini')">Mini</button>
</div>
</div>
</div>
<div class="piano-stage">
<div class="mini-keyboard-panel" id="mini-keyboard-panel">
<div id="note-highway" class="note-highway"></div>
<div class="kb-layout" id="mini-keyboard-layout">
<div id="kb-row-main" class="kb-row"></div>
</div>
</div>
<div class="reference-keyboard-panel" id="full-keyboard-panel">
<div id="full-note-highway" class="full-note-highway">
<div class="full-note-hit-line"></div>
</div>
<div class="reference-keyboard-shell" id="reference-keyboard-shell">
<div class="reference-keyboard" id="reference-keyboard">
<div id="full-kb-whites" class="reference-whites"></div>
<div id="full-kb-blacks" class="reference-blacks"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="layout-splitter panel-splitter"
id="panel-splitter"
role="separator"
aria-label="Resize practice panel"
aria-orientation="vertical"
aria-valuemin="260"
aria-valuemax="520"
aria-valuenow="320"
tabindex="0">
<span></span>
</div>
<aside class="practice-panel">
<section class="practice-card setup-card">
<h4>Practice Setup</h4>
<div class="setup-control-block">
<label for="bpm-input">Starting tempo</label>
<div class="bpm-row">
<input id="bpm-input" type="number" value="60" min="20" max="300">
<span>BPM</span>
</div>
</div>
<div class="tempo-actions setup-actions">
<button id="start-btn" class="btn btn-primary" onclick="handleStartPause()">β–Ά Start</button>
<button id="stop-btn" class="btn btn-danger" onclick="stopPlaying()" disabled>β–  End</button>
</div>
<div class="latency-row">
<label for="latency-slider">Latency</label>
<input id="latency-slider" type="range" min="0" max="500" step="10" value="0"
oninput="setLatencyCompensation(this.value)">
<span id="latency-value">0 ms</span>
</div>
<div id="part-picker" class="part-picker" style="display:none">
<h5>Your part</h5>
<div id="part-buttons" class="part-buttons"></div>
</div>
<div class="accompaniment-row">
<span>Accompaniment</span>
<strong>Other parts</strong>
</div>
</section>
<section class="practice-card performance-card">
<div class="practice-card-header">
<h4>Practice</h4>
<span id="practice-input-status" class="practice-pill">Keyboard</span>
</div>
<div class="next-note-card">
<span>Next note</span>
<div id="next-note-display">β€”</div>
</div>
<div class="status-panel">
<div class="status-row">
<span>Current beat</span>
<span class="status-val" id="beat-val">β€”</span>
</div>
<div class="status-row">
<span>Detected BPM</span>
<span class="status-val" id="tempo-val">β€”</span>
</div>
<div class="status-row">
<span>Following</span>
<span class="status-val" id="practice-following-status">Ready</span>
</div>
</div>
</section>
<section id="guest-tips-card" class="practice-card guest-tips-card" style="display:none">
<h4>Demo Tips</h4>
<ul id="guest-tips-list"></ul>
<div class="guest-annotation-actions">
<button class="btn btn-ghost" type="button" onclick="addGuestAnnotation('Practice this measure slowly')">Mark slow spot</button>
<button class="btn btn-ghost" type="button" onclick="addGuestAnnotation('Good run')">Mark good run</button>
</div>
<div id="guest-annotation-list" class="guest-annotation-list"></div>
</section>
<section class="practice-card input-mode-section">
<h4>Input method</h4>
<div class="input-mode-tabs">
<button class="mode-tab active" id="tab-keyboard" onclick="setInputMode('keyboard')">⌨ Keyboard</button>
<button class="mode-tab" id="tab-mic" onclick="setInputMode('mic')">πŸŽ™ Microphone <span class="beta-badge">Beta</span></button>
</div>
<div id="mic-controls" style="display:none">
<div class="mic-row">
<div class="mic-status-dot" id="mic-dot"></div>
<span id="mic-status-text" style="font-size:.85rem;color:#aaa">Mic off</span>
<span id="mic-note-display" style="font-size:.85rem;font-weight:700;color:#7c6af7;margin-left:auto"></span>
<span id="mic-debug" style="font-size:.75rem;color:#555;margin-left:.75rem;font-family:monospace"></span>
</div>
<div class="mic-row" style="margin-top:.6rem">
<label style="font-size:.8rem;color:#aaa;width:100px">Microphone</label>
<select id="mic-select" class="instr-select" style="flex:1" onchange="onMicChange()">
<option value="">β€” select β€”</option>
</select>
</div>
<div class="mic-row" style="margin-top:.6rem">
<label style="font-size:.8rem;color:#aaa;width:100px">Speaker</label>
<select id="speaker-select" class="instr-select" style="flex:1" onchange="onSpeakerChange()">
<option value="">β€” default β€”</option>
</select>
</div>
<div class="level-meter-wrap" style="margin-top:.75rem">
<div class="level-meter-track" id="level-track">
<div class="level-meter-fill" id="level-fill"></div>
<div class="level-meter-peak" id="level-peak"></div>
<div class="level-meter-gate" id="level-gate"></div>
<span class="level-gate-label" id="level-gate-label">gate</span>
</div>
</div>
<div class="mic-row" style="margin-top:.6rem">
<label style="font-size:.8rem;color:#aaa;width:100px">Noise gate</label>
<input id="noise-gate" type="range" min="1" max="80" value="1"
oninput="onNoiseGateChange(this.value)" style="flex:1">
<span id="noise-gate-val" style="font-size:.8rem;color:#555;width:36px;text-align:right">1</span>
</div>
<p style="font-size:.75rem;color:#555;margin-top:.5rem;line-height:1.5">
The bar shows input level. The red line is the noise gate β€” sounds below it are ignored.<br>
Raise the gate if false notes fire; lower it if your playing isn't registering.
</p>
</div>
</section>
</aside>
</div>
</div>
</div>
</div>
<div id="upgrade-toast" class="upgrade-toast" style="display:none">
<button class="upgrade-toast-close" type="button" onclick="hideUpgradeToast()" aria-label="Dismiss">βœ•</button>
<strong>Create a free account</strong>
<p id="upgrade-toast-message">Save your own pieces and progress across devices.</p>
<div class="upgrade-toast-actions">
<button class="btn btn-primary" type="button" onclick="showCreateAccountPrompt('Create a free account to save your own pieces and progress.')">Create Account</button>
<button class="btn btn-ghost" type="button" onclick="hideUpgradeToast()">Keep exploring</button>
</div>
</div>
<script src="https://unpkg.com/tone@15.1.22/build/Tone.js"></script>
<script src="https://cdn.jsdelivr.net/npm/opensheetmusicdisplay@1.9.2/build/opensheetmusicdisplay.min.js"></script>
<script src="/pitch.js"></script>
<script src="/app.js"></script>
</body>
</html>