Spaces:
Restarting
Restarting
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Import Book from URL β AuthorBot</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #0a0b12; | |
| --surface: #111420; | |
| --surface-2: #181b28; | |
| --border: rgba(255,255,255,0.07); | |
| --border-active: rgba(99,102,241,0.6); | |
| --text: #e2e8f0; | |
| --text-muted: #64748b; | |
| --text-secondary: #94a3b8; | |
| --accent: #6366f1; | |
| --accent-dark: #4f46e5; | |
| --accent-glow: rgba(99,102,241,0.2); | |
| --success: #10b981; | |
| --success-bg: rgba(16,185,129,0.1); | |
| --error: #f87171; | |
| --error-bg: rgba(248,113,113,0.1); | |
| --warning: #fbbf24; | |
| --warning-bg: rgba(251,191,36,0.1); | |
| --radius: 14px; | |
| --radius-sm: 8px; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 24px; | |
| } | |
| /* ββ Modal Shell ββββββββββββββββββββββββββββββββ */ | |
| .modal { | |
| width: 100%; | |
| max-width: 640px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| overflow: hidden; | |
| box-shadow: 0 40px 80px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04) inset; | |
| animation: slideUp 0.35s cubic-bezier(0.22,1,0.36,1); | |
| } | |
| @keyframes slideUp { | |
| from { opacity: 0; transform: translateY(24px) scale(0.98); } | |
| to { opacity: 1; transform: translateY(0) scale(1); } | |
| } | |
| /* ββ Header ββββββββββββββββββββββββββββββββββββ */ | |
| .header { | |
| padding: 28px 32px 24px; | |
| background: linear-gradient(135deg, #111624 0%, #0f1220 100%); | |
| border-bottom: 1px solid var(--border); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .header::before { | |
| content: ''; | |
| position: absolute; | |
| top: -60px; right: -60px; | |
| width: 200px; height: 200px; | |
| background: var(--accent-glow); | |
| border-radius: 50%; | |
| filter: blur(60px); | |
| pointer-events: none; | |
| } | |
| .header-top { | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| margin-bottom: 8px; | |
| } | |
| .header-icon { | |
| width: 44px; height: 44px; | |
| background: linear-gradient(135deg, var(--accent), #818cf8); | |
| border-radius: 12px; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 20px; | |
| flex-shrink: 0; | |
| box-shadow: 0 8px 24px rgba(99,102,241,0.35); | |
| } | |
| .header h1 { | |
| font-size: 1.25rem; | |
| font-weight: 700; | |
| letter-spacing: -0.02em; | |
| } | |
| .header p { | |
| color: var(--text-secondary); | |
| font-size: 0.875rem; | |
| margin-top: 4px; | |
| } | |
| /* ββ Platform pills ββββββββββββββββββββββββββββ */ | |
| .platforms { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| margin-top: 16px; | |
| } | |
| .platform-pill { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 4px 10px; | |
| border-radius: 100px; | |
| background: rgba(255,255,255,0.05); | |
| border: 1px solid var(--border); | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| white-space: nowrap; | |
| } | |
| .platform-pill .dot { | |
| width: 6px; height: 6px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| opacity: 0.8; | |
| } | |
| /* ββ Body ββββββββββββββββββββββββββββββββββββββ */ | |
| .body { | |
| padding: 28px 32px; | |
| } | |
| /* ββ URL Input section βββββββββββββββββββββββββ */ | |
| .url-section label { | |
| display: block; | |
| font-size: 13px; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| margin-bottom: 8px; | |
| letter-spacing: 0.02em; | |
| } | |
| .input-row { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .input-wrap { | |
| flex: 1; | |
| position: relative; | |
| } | |
| .input-wrap svg { | |
| position: absolute; | |
| left: 14px; top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-muted); | |
| pointer-events: none; | |
| } | |
| input[type="url"], input[type="text"], textarea { | |
| width: 100%; | |
| padding: 12px 14px 12px 40px; | |
| border-radius: var(--radius-sm); | |
| border: 1px solid var(--border); | |
| background: var(--surface-2); | |
| color: var(--text); | |
| font-family: inherit; | |
| font-size: 0.9rem; | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| outline: none; | |
| } | |
| input[type="text"].no-icon, textarea { | |
| padding-left: 14px; | |
| } | |
| input[type="url"]:focus, input[type="text"]:focus, textarea:focus { | |
| border-color: var(--border-active); | |
| box-shadow: 0 0 0 3px var(--accent-glow); | |
| } | |
| textarea { | |
| resize: vertical; | |
| min-height: 88px; | |
| } | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 7px; | |
| padding: 12px 20px; | |
| border-radius: var(--radius-sm); | |
| border: none; | |
| font-family: inherit; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| white-space: nowrap; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--accent), #818cf8); | |
| color: white; | |
| box-shadow: 0 4px 16px rgba(99,102,241,0.35); | |
| } | |
| .btn-primary:hover:not(:disabled) { | |
| transform: translateY(-1px); | |
| box-shadow: 0 8px 24px rgba(99,102,241,0.5); | |
| } | |
| .btn-primary:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .btn-ghost { | |
| background: var(--surface-2); | |
| color: var(--text-secondary); | |
| border: 1px solid var(--border); | |
| } | |
| .btn-ghost:hover { background: rgba(255,255,255,0.07); color: var(--text); } | |
| .btn-success { | |
| background: linear-gradient(135deg, #059669, #10b981); | |
| color: white; | |
| box-shadow: 0 4px 16px rgba(16,185,129,0.3); | |
| } | |
| .btn-success:hover:not(:disabled) { | |
| transform: translateY(-1px); | |
| box-shadow: 0 8px 24px rgba(16,185,129,0.45); | |
| } | |
| /* ββ Loader ββββββββββββββββββββββββββββββββββββ */ | |
| .loader { | |
| display: none; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 32px; | |
| gap: 16px; | |
| } | |
| .spinner { | |
| width: 40px; height: 40px; | |
| border: 3px solid rgba(99,102,241,0.2); | |
| border-top-color: var(--accent); | |
| border-radius: 50%; | |
| animation: spin 0.8s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .loader p { color: var(--text-secondary); font-size: 0.875rem; } | |
| /* ββ Alert bar βββββββββββββββββββββββββββββββββ */ | |
| .alert { | |
| display: none; | |
| align-items: flex-start; | |
| gap: 10px; | |
| padding: 14px 16px; | |
| border-radius: var(--radius-sm); | |
| font-size: 0.875rem; | |
| margin-bottom: 20px; | |
| } | |
| .alert.error { background: var(--error-bg); border: 1px solid rgba(248,113,113,0.25); color: var(--error); } | |
| .alert.warning { background: var(--warning-bg); border: 1px solid rgba(251,191,36,0.25); color: var(--warning); } | |
| .alert.success { background: var(--success-bg); border: 1px solid rgba(16,185,129,0.25); color: var(--success); } | |
| /* ββ Preview card ββββββββββββββββββββββββββββββ */ | |
| .preview { | |
| display: none; | |
| background: var(--surface-2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius); | |
| overflow: hidden; | |
| margin-bottom: 24px; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } | |
| .preview-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 14px 16px; | |
| border-bottom: 1px solid var(--border); | |
| background: rgba(255,255,255,0.02); | |
| } | |
| .platform-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 4px 12px; | |
| border-radius: 100px; | |
| background: var(--accent-glow); | |
| border: 1px solid rgba(99,102,241,0.3); | |
| color: #a5b4fc; | |
| font-size: 12px; | |
| font-weight: 600; | |
| } | |
| .confidence-bar-wrap { | |
| margin-left: auto; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .confidence-bar { | |
| width: 80px; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: rgba(255,255,255,0.1); | |
| overflow: hidden; | |
| } | |
| .confidence-fill { | |
| height: 100%; | |
| border-radius: 3px; | |
| background: linear-gradient(90deg, var(--accent), #10b981); | |
| transition: width 0.5s ease; | |
| } | |
| .confidence-label { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| white-space: nowrap; | |
| } | |
| .preview-book { | |
| display: flex; | |
| gap: 20px; | |
| padding: 20px; | |
| } | |
| .preview-cover { | |
| width: 80px; | |
| min-width: 80px; | |
| height: 112px; | |
| border-radius: 8px; | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| overflow: hidden; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 28px; | |
| flex-shrink: 0; | |
| } | |
| .preview-cover img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .preview-info { flex: 1; min-width: 0; } | |
| .preview-info h2 { font-size: 1.05rem; font-weight: 700; margin-bottom: 4px; line-height: 1.3; } | |
| .preview-info .book-author { color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 10px; } | |
| .preview-info .book-desc { | |
| color: var(--text-secondary); | |
| font-size: 0.8rem; | |
| line-height: 1.6; | |
| display: -webkit-box; | |
| -webkit-line-clamp: 3; | |
| -webkit-box-orient: vertical; | |
| overflow: hidden; | |
| } | |
| .preview-meta { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-top: 10px; | |
| } | |
| .meta-chip { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 3px 10px; | |
| border-radius: 100px; | |
| background: rgba(255,255,255,0.04); | |
| border: 1px solid var(--border); | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| } | |
| .meta-chip.highlight { | |
| background: rgba(99,102,241,0.08); | |
| border-color: rgba(99,102,241,0.25); | |
| color: #a5b4fc; | |
| } | |
| /* ββ Author bio panel ββββββββββββββββββββββββββ */ | |
| .author-bio-panel { | |
| margin: 0 20px 20px; | |
| padding: 14px 16px; | |
| border-radius: var(--radius-sm); | |
| background: rgba(255,255,255,0.02); | |
| border: 1px solid var(--border); | |
| border-left: 3px solid var(--accent); | |
| animation: fadeIn 0.3s ease; | |
| } | |
| .author-bio-label { | |
| font-size: 11px; | |
| font-weight: 600; | |
| color: var(--accent); | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| margin-bottom: 8px; | |
| } | |
| .author-bio-text { | |
| font-size: 0.8rem; | |
| color: var(--text-secondary); | |
| line-height: 1.65; | |
| max-height: 120px; | |
| overflow-y: auto; | |
| } | |
| .rating-stars { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 2px; | |
| font-size: 12px; | |
| color: #fbbf24; | |
| } | |
| /* ββ Edit form βββββββββββββββββββββββββββββββββ */ | |
| .edit-form { | |
| display: none; | |
| padding: 0 20px 20px; | |
| border-top: 1px solid var(--border); | |
| } | |
| .edit-toggle { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 12px 20px; | |
| cursor: pointer; | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| transition: color 0.2s; | |
| user-select: none; | |
| } | |
| .edit-toggle:hover { color: var(--text); } | |
| .edit-toggle svg { transition: transform 0.2s; } | |
| .edit-toggle.open svg { transform: rotate(180deg); } | |
| .form-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; | |
| margin-top: 16px; | |
| } | |
| .form-field { display: flex; flex-direction: column; gap: 6px; } | |
| .form-field.full { grid-column: 1 / -1; } | |
| .form-field label { | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: var(--text-secondary); | |
| letter-spacing: 0.02em; | |
| } | |
| /* ββ Action row ββββββββββββββββββββββββββββββββ */ | |
| .actions { | |
| display: flex; | |
| gap: 10px; | |
| justify-content: flex-end; | |
| } | |
| /* ββ Success state βββββββββββββββββββββββββββββ */ | |
| .success-state { | |
| display: none; | |
| flex-direction: column; | |
| align-items: center; | |
| text-align: center; | |
| padding: 40px 32px; | |
| gap: 16px; | |
| animation: fadeIn 0.4s ease; | |
| } | |
| .success-icon { | |
| width: 72px; height: 72px; | |
| background: var(--success-bg); | |
| border: 2px solid rgba(16,185,129,0.3); | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 36px; | |
| } | |
| .success-state h2 { font-size: 1.4rem; font-weight: 700; } | |
| .success-state p { color: var(--text-secondary); font-size: 0.875rem; max-width: 380px; line-height: 1.6; } | |
| .success-book-title { | |
| background: var(--surface-2); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-sm); | |
| padding: 12px 20px; | |
| font-weight: 600; | |
| font-size: 1rem; | |
| max-width: 400px; | |
| width: 100%; | |
| } | |
| .divider { | |
| height: 1px; | |
| background: var(--border); | |
| margin: 20px 0; | |
| } | |
| .step-hint { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 14px 16px; | |
| border-radius: var(--radius-sm); | |
| background: rgba(99,102,241,0.08); | |
| border: 1px solid rgba(99,102,241,0.2); | |
| text-align: left; | |
| font-size: 0.8rem; | |
| color: #a5b4fc; | |
| max-width: 440px; | |
| width: 100%; | |
| } | |
| @media (max-width: 540px) { | |
| body { padding: 12px; align-items: flex-start; } | |
| .header, .body { padding-left: 20px; padding-right: 20px; } | |
| .input-row { flex-direction: column; } | |
| .form-grid { grid-template-columns: 1fr; } | |
| .preview-book { flex-direction: column; } | |
| .preview-cover { width: 60px; min-width: 60px; height: 84px; } | |
| .actions { flex-direction: column-reverse; } | |
| .btn { width: 100%; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="modal" id="importModal"> | |
| <!-- Header --> | |
| <div class="header"> | |
| <div class="header-top"> | |
| <div class="header-icon">π</div> | |
| <div> | |
| <h1>Import Book from URL</h1> | |
| <p>Paste any book link β we'll fetch the details automatically</p> | |
| </div> | |
| </div> | |
| <div class="platforms" id="platformPills"> | |
| <!-- injected by JS --> | |
| </div> | |
| </div> | |
| <!-- Body --> | |
| <div class="body"> | |
| <!-- Alert --> | |
| <div class="alert" id="alert" role="alert"> | |
| <span id="alertIcon"></span> | |
| <span id="alertMsg"></span> | |
| </div> | |
| <!-- Step 1: URL input --> | |
| <div id="stepUrl"> | |
| <div class="url-section"> | |
| <label for="bookUrl">Book URL</label> | |
| <div class="input-row"> | |
| <div class="input-wrap"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/> | |
| <path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/> | |
| </svg> | |
| <input | |
| type="url" | |
| id="bookUrl" | |
| placeholder="https://www.amazon.com/dp/B09... or goodreads.com/book/show/..." | |
| autocomplete="off" | |
| spellcheck="false" | |
| > | |
| </div> | |
| <button class="btn btn-primary" id="fetchBtn" onclick="fetchPreview()"> | |
| <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> | |
| <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/> | |
| </svg> | |
| Fetch | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Loader --> | |
| <div class="loader" id="loader"> | |
| <div class="spinner"></div> | |
| <p id="loaderMsg">Fetching book detailsβ¦</p> | |
| </div> | |
| <!-- Step 2: Preview + edit --> | |
| <div id="stepPreview" style="display:none"> | |
| <!-- Preview card --> | |
| <div class="preview" id="previewCard"> | |
| <div class="preview-header"> | |
| <span class="platform-badge" id="platformBadge">π Platform</span> | |
| <div class="confidence-bar-wrap"> | |
| <div class="confidence-bar"> | |
| <div class="confidence-fill" id="confidenceFill" style="width:0%"></div> | |
| </div> | |
| <span class="confidence-label" id="confidenceLabel">0% match</span> | |
| </div> | |
| </div> | |
| <div class="preview-book"> | |
| <div class="preview-cover" id="previewCover">π</div> | |
| <div class="preview-info"> | |
| <h2 id="previewTitle">β</h2> | |
| <p class="book-author" id="previewAuthor"></p> | |
| <p class="book-desc" id="previewDesc"></p> | |
| <div class="preview-meta" id="previewMeta"></div> | |
| </div> | |
| </div> | |
| <!-- Author Bio Panel (shown when about_author is available) --> | |
| <div class="author-bio-panel" id="authorBioPanel" style="display:none"> | |
| <div class="author-bio-label">βοΈ About the Author</div> | |
| <div class="author-bio-text" id="authorBioText"></div> | |
| </div> | |
| <!-- Edit toggle --> | |
| <div class="edit-toggle" id="editToggle" onclick="toggleEdit()"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/> | |
| <path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/> | |
| </svg> | |
| Edit details | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-left:auto"> | |
| <polyline points="6 9 12 15 18 9"/> | |
| </svg> | |
| </div> | |
| <div class="edit-form" id="editForm"> | |
| <div class="form-grid"> | |
| <div class="form-field full"> | |
| <label>Title</label> | |
| <input type="text" class="no-icon" id="editTitle" placeholder="Book title"> | |
| </div> | |
| <div class="form-field"> | |
| <label>Author</label> | |
| <input type="text" class="no-icon" id="editAuthor" placeholder="Author name"> | |
| </div> | |
| <div class="form-field"> | |
| <label>Genre</label> | |
| <input type="text" class="no-icon" id="editGenre" placeholder="e.g. Fiction"> | |
| </div> | |
| <div class="form-field"> | |
| <label>ISBN</label> | |
| <input type="text" class="no-icon" id="editISBN" placeholder="ISBN-13 or ISBN-10"> | |
| </div> | |
| <div class="form-field"> | |
| <label>Cover Image URL <span style="color:var(--text-muted)">(optional)</span></label> | |
| <input type="text" class="no-icon" id="editCoverUrl" placeholder="https://..."> | |
| </div> | |
| <div class="form-field full"> | |
| <label>Description / Synopsis</label> | |
| <textarea class="no-icon" id="editDesc" placeholder="Short description or back-cover copyβ¦"></textarea> | |
| </div> | |
| <div class="form-field full"> | |
| <label>About the Author</label> | |
| <textarea class="no-icon" id="editAboutAuthor" placeholder="Author biographyβ¦" style="min-height:80px"></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Actions --> | |
| <div class="actions"> | |
| <button class="btn btn-ghost" onclick="resetForm()"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <polyline points="15 18 9 12 15 6"/> | |
| </svg> | |
| Back | |
| </button> | |
| <button class="btn btn-success" id="importBtn" onclick="confirmImport()"> | |
| <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> | |
| <polyline points="20 6 9 17 4 12"/> | |
| </svg> | |
| Add to My Books | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Step 3: Success --> | |
| <div class="success-state" id="stepSuccess"> | |
| <div class="success-icon">β </div> | |
| <h2>Book Added!</h2> | |
| <p id="successMsg">Your book has been imported and is ready. Upload a PDF or EPUB to enable AI-powered chat.</p> | |
| <div class="success-book-title" id="successTitle"></div> | |
| <div class="step-hint"> | |
| π‘ <span>Now go to <strong>Books β Upload PDF/EPUB</strong> to enable readers to ask questions about this book.</span> | |
| </div> | |
| <div style="display:flex;gap:10px;margin-top:8px"> | |
| <button class="btn btn-ghost" onclick="resetForm()">Import another</button> | |
| <button class="btn btn-primary" onclick="goToBooks()">Go to Books</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const API = window.location.origin + '/api'; | |
| const TOKEN = localStorage.getItem('access_token') || ''; | |
| const SLUG = localStorage.getItem('author_slug') || ''; | |
| const PLATFORMS = [ | |
| { name: 'Amazon', icon: 'π' }, | |
| { name: 'Barnes & Noble', icon: 'π' }, | |
| { name: 'Goodreads', icon: 'β' }, | |
| { name: 'Google Books', icon: 'π' }, | |
| { name: 'Apple Books', icon: 'π' }, | |
| { name: 'Kobo', icon: 'π' }, | |
| { name: 'ThriftBooks', icon: 'πΈ' }, | |
| { name: 'AbeBooks', icon: 'ποΈ' }, | |
| { name: 'WorldCat', icon: 'π' }, | |
| { name: 'Open Library', icon: 'π' }, | |
| ]; | |
| // ββ Render platform pills ββββββββββββββββββββββββββββββββββββββββββββββ | |
| const pillsEl = document.getElementById('platformPills'); | |
| PLATFORMS.forEach(p => { | |
| pillsEl.insertAdjacentHTML('beforeend', | |
| `<span class="platform-pill"><span class="dot"></span>${p.icon} ${p.name}</span>` | |
| ); | |
| }); | |
| // ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let _currentMeta = null; | |
| // ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function show(id) { document.getElementById(id).style.display = ''; } | |
| function hide(id) { document.getElementById(id).style.display = 'none'; } | |
| function flex(id) { document.getElementById(id).style.display = 'flex'; } | |
| function block(id) { document.getElementById(id).style.display = 'block'; } | |
| function showAlert(type, msg) { | |
| const el = document.getElementById('alert'); | |
| const icons = { error: 'β', warning: 'β οΈ', success: 'β ' }; | |
| document.getElementById('alertIcon').textContent = icons[type] || ''; | |
| document.getElementById('alertMsg').textContent = msg; | |
| el.className = `alert ${type}`; | |
| el.style.display = 'flex'; | |
| } | |
| function hideAlert() { | |
| document.getElementById('alert').style.display = 'none'; | |
| } | |
| function setLoading(msg) { | |
| hide('stepUrl'); | |
| hide('stepPreview'); | |
| hide('stepSuccess'); | |
| hideAlert(); | |
| document.getElementById('loaderMsg').textContent = msg || 'Fetching book detailsβ¦'; | |
| flex('loader'); | |
| } | |
| function stopLoading() { hide('loader'); } | |
| // ββ Step 1 β Step 2: Fetch preview βββββββββββββββββββββββββββββββββββββ | |
| async function fetchPreview() { | |
| const url = document.getElementById('bookUrl').value.trim(); | |
| if (!url) { showAlert('error', 'Please paste a book URL first.'); return; } | |
| if (!url.startsWith('http')) { showAlert('error', 'Please enter a full URL starting with https://'); return; } | |
| setLoading('Detecting platform and fetching metadataβ¦'); | |
| try { | |
| const res = await fetch(`${API}/admin/${SLUG}/books/import-url/preview`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': 'Bearer ' + TOKEN, | |
| }, | |
| body: JSON.stringify({ url, force_refresh: true }), | |
| }); | |
| const data = await res.json(); | |
| stopLoading(); | |
| if (!res.ok) { | |
| show('stepUrl'); | |
| showAlert('error', data.detail || 'Failed to fetch book details.'); | |
| return; | |
| } | |
| _currentMeta = data; | |
| renderPreview(data); | |
| } catch (e) { | |
| stopLoading(); | |
| show('stepUrl'); | |
| showAlert('error', 'Network error: ' + e.message); | |
| } | |
| } | |
| // ββ Render preview card ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function renderPreview(data) { | |
| // Platform badge | |
| const platformIcons = { | |
| amazon: 'π', barnes_noble: 'π', goodreads: 'β', | |
| google_books: 'π', apple_books: 'π', kobo: 'π', | |
| thriftbooks: 'πΈ', abebooks: 'ποΈ', worldcat: 'π', open_library: 'π', | |
| }; | |
| const icon = platformIcons[data.platform] || 'π'; | |
| document.getElementById('platformBadge').textContent = `${icon} ${data.platform_display || data.platform}`; | |
| // Confidence | |
| const pct = Math.round((data.confidence || 0) * 100); | |
| document.getElementById('confidenceFill').style.width = pct + '%'; | |
| document.getElementById('confidenceLabel').textContent = pct + '% matched'; | |
| // Cover | |
| const coverEl = document.getElementById('previewCover'); | |
| if (data.cover_url) { | |
| coverEl.innerHTML = `<img src="${esc(data.cover_url)}" alt="cover" onerror="this.parentElement.textContent='π'">`; | |
| } else { | |
| coverEl.textContent = 'π'; | |
| } | |
| // Text | |
| document.getElementById('previewTitle').textContent = data.title || '(Title not found)'; | |
| document.getElementById('previewAuthor').textContent = data.author ? 'by ' + data.author : ''; | |
| document.getElementById('previewDesc').textContent = data.description || ''; | |
| // Meta chips β comprehensive set of all scraped fields | |
| const chips = []; | |
| if (data.rating) { | |
| const stars = parseFloat(data.rating); | |
| const filled = Math.round(stars); | |
| const starStr = 'β '.repeat(filled) + 'β'.repeat(5 - filled); | |
| const countStr = data.rating_count ? ` (${data.rating_count})` : ''; | |
| chips.push({ text: `<span class="rating-stars">${starStr}</span> ${data.rating}${countStr}`, highlight: true }); | |
| } | |
| if (data.genre) chips.push({ text: `π·οΈ ${data.genre}` }); | |
| if (data.page_count) chips.push({ text: `π ${data.page_count} pages` }); | |
| if (data.language && data.language.toLowerCase() !== 'en') | |
| chips.push({ text: `π ${data.language}` }); | |
| if (data.publisher) chips.push({ text: `π’ ${data.publisher}` }); | |
| if (data.publish_year) chips.push({ text: `π ${data.publish_year}` }); | |
| if (data.isbn) chips.push({ text: `π’ ISBN ${data.isbn}` }); | |
| document.getElementById('previewMeta').innerHTML = | |
| chips.map(c => `<span class="meta-chip${c.highlight ? ' highlight':''}">${ c.text }</span>`).join(''); | |
| // Pre-fill edit fields | |
| document.getElementById('editTitle').value = data.title || ''; | |
| document.getElementById('editAuthor').value = data.author || ''; | |
| document.getElementById('editGenre').value = data.genre || ''; | |
| document.getElementById('editISBN').value = data.isbn || ''; | |
| document.getElementById('editCoverUrl').value = data.cover_url || ''; | |
| document.getElementById('editDesc').value = data.description || ''; | |
| document.getElementById('editAboutAuthor').value = data.about_author || ''; | |
| // Alert handling | |
| if (data.error && !data.title) { | |
| showAlert('error', data.error + ' β please fill in the details manually.'); | |
| } else if (data.error || (data.warnings && data.warnings.length)) { | |
| const msg = data.error || data.warnings.join('; '); | |
| showAlert('warning', 'β οΈ Partial data β some fields enriched via Google Books/Open Library: ' + msg); | |
| } else { | |
| hideAlert(); | |
| } | |
| // Show author bio panel (outside preview-book div, inside .preview card) | |
| const bioPanel = document.getElementById('authorBioPanel'); | |
| if (data.about_author) { | |
| bioPanel.style.display = 'block'; | |
| document.getElementById('authorBioText').textContent = data.about_author; | |
| } else { | |
| bioPanel.style.display = 'none'; | |
| } | |
| // Show preview | |
| block('previewCard'); | |
| show('stepPreview'); | |
| show('stepUrl'); | |
| } | |
| // ββ Edit toggle βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function toggleEdit() { | |
| const toggle = document.getElementById('editToggle'); | |
| const form = document.getElementById('editForm'); | |
| const open = toggle.classList.toggle('open'); | |
| form.style.display = open ? 'block' : 'none'; | |
| } | |
| // ββ Step 2 β Step 3: Confirm import βββββββββββββββββββββββββββββββββββββββ | |
| async function confirmImport() { | |
| if (!_currentMeta) return; | |
| const title = (document.getElementById('editTitle').value.trim() | |
| || _currentMeta.title || '').trim(); | |
| if (!title) { | |
| showAlert('error', 'Please enter a title before importing.'); | |
| return; | |
| } | |
| document.getElementById('importBtn').disabled = true; | |
| setLoading('Saving book to your libraryβ¦'); | |
| try { | |
| const body = { | |
| url: _currentMeta.source_url, | |
| title: document.getElementById('editTitle').value.trim(), | |
| author: document.getElementById('editAuthor').value.trim(), | |
| description: document.getElementById('editDesc').value.trim(), | |
| cover_url: document.getElementById('editCoverUrl').value.trim(), | |
| isbn: document.getElementById('editISBN').value.trim(), | |
| genre: document.getElementById('editGenre').value.trim(), | |
| }; | |
| const res = await fetch(`${API}/admin/${SLUG}/books/import-url`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': 'Bearer ' + TOKEN, | |
| }, | |
| body: JSON.stringify(body), | |
| }); | |
| const data = await res.json(); | |
| stopLoading(); | |
| document.getElementById('importBtn').disabled = false; | |
| if (!res.ok) { | |
| show('stepPreview'); | |
| showAlert('error', data.detail || 'Import failed.'); | |
| return; | |
| } | |
| // Success! | |
| const coverUrl = document.getElementById('editCoverUrl').value.trim() | |
| || _currentMeta?.cover_url || ''; | |
| showSuccess(data, coverUrl); | |
| } catch (e) { | |
| stopLoading(); | |
| document.getElementById('importBtn').disabled = false; | |
| show('stepPreview'); | |
| showAlert('error', 'Network error: ' + e.message); | |
| } | |
| } | |
| // ββ Step 3: Success βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function showSuccess(data, coverUrl) { | |
| document.getElementById('successTitle').textContent = data.title || 'Book imported'; | |
| document.getElementById('successMsg').textContent = | |
| `Imported from ${data.platform || 'external source'}. ` + | |
| (data.next_steps || 'Upload a PDF/EPUB to enable AI chat.'); | |
| flex('stepSuccess'); | |
| } | |
| function goToBooks() { | |
| // Navigate to the books section of the admin SPA | |
| const adminUrl = window.location.origin + '/admin'; | |
| if (window.opener) { | |
| window.opener.postMessage({ type: 'navigate', section: 'books' }, '*'); | |
| window.close(); | |
| } else { | |
| window.location.href = adminUrl + '#books'; | |
| } | |
| } | |
| // ββ Reset ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function resetForm() { | |
| _currentMeta = null; | |
| document.getElementById('bookUrl').value = ''; | |
| document.getElementById('importBtn').disabled = false; | |
| hide('stepPreview'); | |
| hide('stepSuccess'); | |
| hide('loader'); | |
| hideAlert(); | |
| show('stepUrl'); | |
| // Close edit form | |
| document.getElementById('editForm').style.display = 'none'; | |
| document.getElementById('editToggle').classList.remove('open'); | |
| } | |
| // ββ Keyboard shortcut βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| document.getElementById('bookUrl').addEventListener('keydown', e => { | |
| if (e.key === 'Enter') fetchPreview(); | |
| }); | |
| // ββ Security: escape for display βββββββββββββββββββββββββββββββββββββββββ | |
| function esc(str) { | |
| return String(str) | |
| .replace(/&/g,'&').replace(/</g,'<') | |
| .replace(/>/g,'>').replace(/"/g,'"'); | |
| } | |
| // ββ Auth guard ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if (!TOKEN || !SLUG) { | |
| document.querySelector('.body').innerHTML = | |
| `<div style="text-align:center;padding:40px;color:var(--text-secondary)"> | |
| <div style="font-size:40px;margin-bottom:12px">π</div> | |
| <p style="margin-bottom:16px">You need to be signed in to import books.</p> | |
| <a href="/static/admin.html" style="color:var(--accent);text-decoration:none;font-weight:600">β Sign In</a> | |
| </div>`; | |
| } | |
| </script> | |
| </body> | |
| </html> | |