323 lines
14 KiB
HTML
323 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Spectra — A YouTube Suggest Engine</title>
|
|
<meta name="description" content="Better YouTube recommendations with zero repeats, steerable topics & people, recency bias, and persistent watch list.">
|
|
<link rel="icon" type="image/svg+xml" href="/icon.svg">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="/styles.css">
|
|
</head>
|
|
<body>
|
|
<!-- Ambient background glow elements -->
|
|
<div class="ambient-glow glow-1"></div>
|
|
<div class="ambient-glow glow-2"></div>
|
|
|
|
<!-- Top App Navigation -->
|
|
<header class="app-header">
|
|
<div class="header-left">
|
|
<div class="brand">
|
|
<img src="/icon.svg" alt="Spectra Logo" class="brand-logo">
|
|
<div class="brand-text">
|
|
<span class="brand-title">SPECTRA</span>
|
|
<span class="brand-subtitle">A YouTube Suggest Engine</span>
|
|
</div>
|
|
</div>
|
|
<div class="header-pill-badge">
|
|
<span class="pulse-dot"></span>
|
|
<span id="headerStatusText">No Repeats Active</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="header-center">
|
|
<div class="recency-picker" title="Bias recommendations toward newer videos">
|
|
<span class="recency-label">Timing:</span>
|
|
<button class="recency-btn" data-bias="ultra_fresh">⚡ Ultra Fresh (<48h)</button>
|
|
<button class="recency-btn active" data-bias="recent">📅 Recent (<30d)</button>
|
|
<button class="recency-btn" data-bias="broad">🌐 Broad</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="header-right">
|
|
<button id="btnRegenerate" class="action-btn btn-primary" title="Suggest fresh batch of videos with no repeats">
|
|
<svg class="btn-icon spin-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/>
|
|
</svg>
|
|
<span>Suggest Next Batch</span>
|
|
<kbd class="kbd-shortcut">R</kbd>
|
|
</button>
|
|
|
|
<button id="btnOpenWatchlist" class="action-btn btn-glass" title="View preserved Watch List">
|
|
<svg class="btn-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
|
|
</svg>
|
|
<span>Watch List</span>
|
|
<span id="watchlistCountBadge" class="count-badge">0</span>
|
|
</button>
|
|
|
|
<button id="btnOpenTuning" class="action-btn btn-glass icon-only" title="Algorithm Tuning & Steering">
|
|
<svg class="btn-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 20v-6M6 20V10M18 20V4M2 10h8M14 14h8M8 4h8"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Interactive Topics & People Ribbon -->
|
|
<section class="topics-ribbon">
|
|
<div class="ribbon-label">
|
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
|
</svg>
|
|
<span>Focus Stream:</span>
|
|
</div>
|
|
<div id="topicsContainer" class="topics-scroll">
|
|
<button class="topic-chip active" data-topic-id="all">
|
|
<span>✨ All Topics & People</span>
|
|
</button>
|
|
<!-- Injected dynamically via app.js -->
|
|
</div>
|
|
<button id="btnAddTopic" class="btn-add-chip" title="Add a custom topic or YouTube creator">
|
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="12" y1="5" x2="12" y2="19"/>
|
|
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
</svg>
|
|
<span>Add Topic/Person</span>
|
|
</button>
|
|
</section>
|
|
|
|
<!-- Main Feed Area -->
|
|
<main class="main-content">
|
|
<div class="feed-header">
|
|
<div class="feed-title-wrap">
|
|
<h2 id="currentFeedTitle">Curated Suggestions</h2>
|
|
<span id="feedSubtitle" class="feed-subtitle">No repeats • Newer bias • Steered by your taste</span>
|
|
</div>
|
|
<div class="feed-meta">
|
|
<span class="meta-pill" id="seenCounterPill" title="Videos previously suggested and prevented from repeating">
|
|
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
|
<circle cx="12" cy="12" r="3"/>
|
|
</svg>
|
|
<span id="seenCountText">0 Seen</span>
|
|
</span>
|
|
<button id="btnRefreshCatalog" class="btn-text-action" title="Re-query candidates">
|
|
Refresh Candidates
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Video Grid -->
|
|
<div id="videoGrid" class="video-grid">
|
|
<!-- Loading skeletons or dynamic cards -->
|
|
</div>
|
|
|
|
<!-- Empty or Exhausted state -->
|
|
<div id="emptyState" class="empty-state hidden">
|
|
<div class="empty-icon-wrap">
|
|
<svg viewBox="0 0 24 24" width="36" height="36" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
</svg>
|
|
</div>
|
|
<h3>All candidate suggestions seen!</h3>
|
|
<p>Spectra guarantees no repeats and has shown every un-noped video in current topics. You can add new topics, fetch live YouTube queries, or reset your seen history.</p>
|
|
<div class="empty-actions">
|
|
<button id="btnResetSeenEmpty" class="action-btn btn-primary">Reset Seen History</button>
|
|
<button id="btnFetchMoreLive" class="action-btn btn-glass">Search YouTube for More</button>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Watch List Drawer (Preserved Across Regenerations) -->
|
|
<div id="watchlistDrawer" class="drawer-overlay hidden">
|
|
<div class="drawer-panel">
|
|
<div class="drawer-header">
|
|
<div class="drawer-title-wrap">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#6366f1" stroke-width="2">
|
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
|
|
</svg>
|
|
<h3>Your Watch List</h3>
|
|
<span class="preserved-tag">Preserved across batches</span>
|
|
</div>
|
|
<button id="btnCloseWatchlist" class="btn-close-drawer">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="drawer-tabs">
|
|
<button class="drawer-tab active" data-filter="all">All (<span id="wlTabAll">0</span>)</button>
|
|
<button class="drawer-tab" data-filter="unwatched">Unwatched (<span id="wlTabUnwatched">0</span>)</button>
|
|
<button class="drawer-tab" data-filter="watched">Watched (<span id="wlTabWatched">0</span>)</button>
|
|
</div>
|
|
|
|
<div id="watchlistItems" class="watchlist-items-scroll">
|
|
<!-- Injected dynamically -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Algorithm Tuning & Steering Drawer -->
|
|
<div id="tuningDrawer" class="drawer-overlay hidden">
|
|
<div class="drawer-panel">
|
|
<div class="drawer-header">
|
|
<div class="drawer-title-wrap">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#06b6d4" stroke-width="2">
|
|
<path d="M12 20v-6M6 20V10M18 20V4M2 10h8M14 14h8M8 4h8"/>
|
|
</svg>
|
|
<h3>Algorithm Steering & Tuning</h3>
|
|
</div>
|
|
<button id="btnCloseTuning" class="btn-close-drawer">
|
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="tuning-content">
|
|
<!-- Telemetry Summary -->
|
|
<div class="telemetry-grid">
|
|
<div class="telemetry-card">
|
|
<span class="telemetry-val" id="statThumbsUp">0</span>
|
|
<span class="telemetry-key">👍 Upvotes</span>
|
|
</div>
|
|
<div class="telemetry-card">
|
|
<span class="telemetry-val" id="statThumbsDown">0</span>
|
|
<span class="telemetry-key">👎 Downvotes</span>
|
|
</div>
|
|
<div class="telemetry-card">
|
|
<span class="telemetry-val" id="statBigNopes">0</span>
|
|
<span class="telemetry-key">🚫 Big Nopes</span>
|
|
</div>
|
|
<div class="telemetry-card">
|
|
<span class="telemetry-val" id="statTotalSeen">0</span>
|
|
<span class="telemetry-key">👁️ Seen (No Repeats)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Topics & Weights Steering -->
|
|
<div class="tuning-section">
|
|
<h4>Active Topic & Creator Weights</h4>
|
|
<p class="section-desc">Upvotes and downvotes steer these affinity weights automatically. You can also toggle topics on/off or fine-tune them.</p>
|
|
<div id="tuningTopicsList" class="tuning-topics-list">
|
|
<!-- Dynamic topic sliders -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Big Nopes Manager -->
|
|
<div class="tuning-section">
|
|
<h4>The Big Nopes (Blocklist)</h4>
|
|
<p class="section-desc">Channels or videos you marked with a Big Nope. Permanently excluded from all suggestions.</p>
|
|
<div id="nopesList" class="nopes-list">
|
|
<!-- Dynamic nopes -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Seen History Reset -->
|
|
<div class="tuning-section">
|
|
<h4>Seen History Management</h4>
|
|
<p class="section-desc">Spectra remembers all suggested videos so you never see repeats. If you want to cycle back through everything, reset here.</p>
|
|
<button id="btnResetSeenHistory" class="action-btn btn-danger">
|
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="1 4 1 10 7 10"/>
|
|
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
|
|
</svg>
|
|
<span>Reset Seen Videos History</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Topic / Person Modal -->
|
|
<div id="addTopicModal" class="modal-overlay hidden">
|
|
<div class="modal-dialog">
|
|
<div class="modal-header">
|
|
<h3>Add Stream or Creator</h3>
|
|
<button id="btnCloseAddModal" class="btn-close-modal">×</button>
|
|
</div>
|
|
<form id="addTopicForm" class="modal-body">
|
|
<div class="form-group">
|
|
<label for="topicNameInput">Name / Title</label>
|
|
<input type="text" id="topicNameInput" placeholder="e.g. Vintage Audio Restoration or Jeff Geerling" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="topicTypeSelect">Type</label>
|
|
<select id="topicTypeSelect">
|
|
<option value="topic">Topic / Interest</option>
|
|
<option value="person">Person / Creator</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="topicQueryInput">YouTube Search Query or Keywords</label>
|
|
<input type="text" id="topicQueryInput" placeholder="e.g. vintage tube amp repair vinyl hi-fi">
|
|
<small>Leave blank to search by the name.</small>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" id="btnCancelAddModal" class="action-btn btn-glass">Cancel</button>
|
|
<button type="submit" class="action-btn btn-primary">Add to Spectra</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Video Player Modal -->
|
|
<div id="playerModal" class="player-overlay hidden">
|
|
<div class="player-container">
|
|
<div class="player-header">
|
|
<div class="player-title-info">
|
|
<h4 id="playerVideoTitle">Loading video...</h4>
|
|
<span id="playerVideoChannel">Creator</span>
|
|
</div>
|
|
<div class="player-header-actions">
|
|
<a id="playerExternalLink" href="#" target="_blank" rel="noopener noreferrer" class="btn-icon-link" title="Open directly in YouTube">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
<polyline points="15 3 21 3 21 9"/>
|
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
</svg>
|
|
</a>
|
|
<button id="btnClosePlayer" class="btn-close-player" title="Close player (Esc)">
|
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="player-video-wrap">
|
|
<iframe id="playerIframe" src="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
|
|
</div>
|
|
<div class="player-footer">
|
|
<div class="player-footer-left">
|
|
<button id="playerBtnWatchlist" class="action-btn btn-glass">
|
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
|
|
</svg>
|
|
<span id="playerWatchlistText">Add to Watch List</span>
|
|
</button>
|
|
</div>
|
|
<div class="player-footer-right">
|
|
<button id="playerBtnThumbsUp" class="card-btn btn-thumb-up" title="Thumbs Up">👍 Boost</button>
|
|
<button id="playerBtnThumbsDown" class="card-btn btn-thumb-down" title="Thumbs Down">👎 Less</button>
|
|
<button id="playerBtnBigNope" class="card-btn btn-nope" title="Big Nope">🚫 Big Nope</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast Notification System -->
|
|
<div id="toastContainer" class="toast-container"></div>
|
|
|
|
<script type="module" src="/app.js"></script>
|
|
</body>
|
|
</html>
|