| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AI Digital Library</title> |
| <style> |
| :root { |
| --primary: #2c3e50; |
| --accent: #3498db; |
| --bg: #f4f7f6; |
| --card-bg: #ffffff; |
| --text: #333; |
| } |
| |
| body { |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| background-color: var(--bg); |
| color: var(--text); |
| margin: 0; |
| padding: 0; |
| line-height: 1.6; |
| } |
| |
| header { |
| background-color: var(--primary); |
| color: white; |
| padding: 1rem 2rem; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); |
| } |
| |
| header h1 { |
| margin: 0; |
| font-size: 1.5rem; |
| font-weight: 300; |
| } |
| |
| header a { |
| color: white; |
| text-decoration: none; |
| font-size: 0.8rem; |
| opacity: 0.8; |
| border: 1px solid rgba(255, 255, 255, 0.3); |
| padding: 5px 10px; |
| border-radius: 4px; |
| transition: opacity 0.3s; |
| } |
| |
| header a:hover { |
| opacity: 1; |
| background: rgba(255, 255, 255, 0.1); |
| } |
| |
| nav { |
| background: white; |
| padding: 0 2rem; |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); |
| margin-bottom: 2rem; |
| display: flex; |
| flex-wrap: wrap; |
| gap: 10px; |
| } |
| |
| nav button { |
| background: none; |
| border: none; |
| padding: 1rem 1.5rem; |
| font-size: 1rem; |
| color: var(--text); |
| cursor: pointer; |
| border-bottom: 3px solid transparent; |
| transition: all 0.3s; |
| } |
| |
| nav button.active { |
| color: var(--accent); |
| border-bottom: 3px solid var(--accent); |
| font-weight: bold; |
| } |
| |
| main { |
| max-width: 1000px; |
| margin: 0 auto; |
| padding: 0 2rem; |
| } |
| |
| .section { |
| display: none; |
| animation: fadeIn 0.5s; |
| } |
| |
| .section.active { |
| display: block; |
| } |
| |
| @keyframes fadeIn { |
| from { |
| opacity: 0; |
| transform: translateY(10px); |
| } |
| |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| .card-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
| gap: 20px; |
| } |
| |
| .card { |
| background: var(--card-bg); |
| border-radius: 8px; |
| padding: 20px; |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); |
| transition: transform 0.2s; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .card:hover { |
| transform: translateY(-5px); |
| } |
| |
| .card h3 { |
| color: var(--primary); |
| margin-top: 0; |
| } |
| |
| .card .meta { |
| font-size: 0.85rem; |
| color: #777; |
| margin-bottom: 10px; |
| display: flex; |
| justify-content: space-between; |
| } |
| |
| .tag { |
| background: #eee; |
| padding: 2px 6px; |
| border-radius: 4px; |
| font-size: 0.75rem; |
| margin-right: 5px; |
| display: inline-block; |
| } |
| |
| input[type="text"], |
| textarea { |
| width: 100%; |
| padding: 12px; |
| border: 1px solid #ddd; |
| border-radius: 6px; |
| font-size: 1rem; |
| margin-bottom: 10px; |
| box-sizing: box; |
| font-family: inherit; |
| } |
| |
| textarea { |
| height: 150px; |
| resize: vertical; |
| } |
| |
| button.action-btn { |
| background-color: var(--accent); |
| color: white; |
| border: none; |
| padding: 10px 20px; |
| border-radius: 6px; |
| font-size: 1rem; |
| cursor: pointer; |
| transition: background 0.3s; |
| } |
| |
| button.action-btn:hover { |
| background-color: #2980b9; |
| } |
| |
| button.action-btn:disabled { |
| background-color: #ccc; |
| cursor: not-allowed; |
| } |
| |
| .status-bar { |
| background: #e8f4fc; |
| padding: 10px; |
| border-radius: 6px; |
| margin-bottom: 20px; |
| font-size: 0.9rem; |
| color: var(--primary); |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| } |
| |
| .spinner { |
| width: 20px; |
| height: 20px; |
| border: 3px solid rgba(0, 0, 0, 0.1); |
| border-top: 3px solid var(--accent); |
| border-radius: 50%; |
| animation: spin 1s linear infinite; |
| } |
| |
| @keyframes spin { |
| 0% { |
| transform: rotate(0deg); |
| } |
| |
| 100% { |
| transform: rotate(360deg); |
| } |
| } |
| |
| .result-box { |
| background: #f8f9fa; |
| border-left: 4px solid var(--accent); |
| padding: 15px; |
| margin-top: 15px; |
| white-space: pre-wrap; |
| } |
| |
| .highlight-match { |
| background-color: #fff3cd; |
| padding: 2px 4px; |
| border-radius: 2px; |
| } |
| |
| .progress-bar { |
| width: 100%; |
| height: 4px; |
| background: #ddd; |
| border-radius: 2px; |
| overflow: hidden; |
| margin-top: 5px; |
| } |
| |
| .progress-fill { |
| height: 100%; |
| background: var(--accent); |
| transition: width 0.3s; |
| } |
| |
| .error-message { |
| background: #ffebee; |
| border-left: 4px solid #f44336; |
| padding: 15px; |
| margin-top: 15px; |
| color: #c62828; |
| } |
| </style> |
| </head> |
|
|
| <body> |
|
|
| <header> |
| <h1>📚 AI Digital Library</h1> |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">Built with anycoder</a> |
| </header> |
|
|
| <nav> |
| <button onclick="switchTab('library')" class="active" id="tab-library">Library View</button> |
| <button onclick="switchTab('search')" id="tab-search">Semantic Search</button> |
| <button onclick="switchTab('summarize')" id="tab-summarize">Book Summarizer</button> |
| <button onclick="switchTab('classify')" id="tab-classify">Auto-Tagging</button> |
| </nav> |
|
|
| <main> |
| |
| <div id="status-container" class="status-bar" style="display:none;"> |
| <div class="spinner"></div> |
| <span id="status-text">Initializing AI Models...</span> |
| <div class="progress-bar"> |
| <div class="progress-fill" id="progress-fill" style="width: 0%"></div> |
| </div> |
| </div> |
|
|
| |
| <div id="library" class="section active"> |
| <h2>Current Collection</h2> |
| <p>Your local digital library containing classic texts and summaries.</p> |
| <div class="card-grid" id="library-grid"> |
| |
| </div> |
| </div> |
|
|
| |
| <div id="search" class="section"> |
| <h2>Semantic Search</h2> |
| <p>Find books based on meaning, not just keywords. Try searching for "war" to find books about conflict, or "love" |
| to find romance.</p> |
| <input type="text" id="search-input" placeholder="Describe what you are looking for..."> |
| <button class="action-btn" onclick="performSearch()" id="search-btn">Search Library</button> |
| <div id="search-results" class="card-grid" style="margin-top: 20px;"></div> |
| </div> |
|
|
| |
| <div id="summarize" class="section"> |
| <h2>Text Summarizer</h2> |
| <p>Paste a long chapter or article description to get an instant AI summary.</p> |
| <textarea id="summarize-input" placeholder="Paste text here (e.g., a book synopsis)..."></textarea> |
| <button class="action-btn" onclick="performSummarization()" id="summarize-btn">Generate Summary</button> |
| <div id="summarize-output" class="result-box"></div> |
| </div> |
|
|
| |
| <div id="classify" class="section"> |
| <h2>Automatic Tagging</h2> |
| <p>Enter a book description and let AI suggest the best categories for your library.</p> |
| <textarea id="classify-input" placeholder="Enter book description..."></textarea> |
| <button class="action-btn" onclick="performClassification()" id="classify-btn">Generate Tags</button> |
| <div id="classify-output" class="result-box"></div> |
| </div> |
| </main> |
|
|
| <script type="module"> |
| import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0'; |
| |
| // Configuration |
| env.useLocalModels = false; // Use remote models from Hugging Face |
| env.allowRemoteModels = true; |
| |
| // State Management |
| let embedder = null; |
| let summarizer = null; |
| let classifier = null; |
| let isModelsLoaded = false; |
| |
| // Mock Library Data |
| const libraryData = [ |
| { |
| id: 1, |
| title: "The Great Gatsby", |
| author: "F. Scott Fitzgerald", |
| year: 1925, |
| text: "The story primarily concerns the young and mysterious millionaire Jay Gatsby and his quixotic passion and |
| obsession for the beautiful former debutante Daisy Buchanan.", |
| tags: ["Fiction", "Classic", "Romance"] |
| }, |
| { |
| id: 2, |
| title: "1984", |
| author: "George Orwell", |
| year: 1949, |
| text: "Set in an imagined future where Great Britain has fallen to a totalitarian regime, the story follows Winston |
| Smith, a low-ranking member of 'the Party', who is frustrated by the omnipresent eyes of the party, and its ominous |
| ruler Big Brother.", |
| tags: ["Sci-Fi", "Political", "Dystopian"] |
| }, |
| { |
| id: 3, |
| title: "To Kill a Mockingbird", |
| author: "Harper Lee", |
| year: 1960, |
| text: "The story takes place during the Great Depression in the fictional town of Maycomb, Alabama. It focuses on |
| six-year-old Jean Louise Finch (known as Scout), who lives with her older brother Jeremy (known as Jem) and their father |
| Atticus, a middle-aged lawyer.", |
| tags: ["Historical", "Legal", "Drama"] |
| }, |
| { |
| id: 4, |
| title: "Dune", |
| author: "Frank Herbert", |
| year: 1965, |
| text: "Set on the desert planet Arrakis, the story follows young Paul Atreides as his family accepts stewardship of the |
| planet Arrakis. While the planet is an inhospitable and sparsely populated desert wasteland, it is the only source of |
| melange, or 'the spice', a drug which extends life and enhances mental abilities.", |
| tags: ["Sci-Fi", "Adventure", "Space"] |
| }, |
| { |
| id: 5, |
| title: "Pride and Prejudice", |
| author: "Jane Austen", |
| year: 1813, |
| text: "The story follows the emotional development of Elizabeth Bennet, who learns the error of her ways and hard |
| prejudices, through the plot of the novel.", |
| tags: ["Romance", "Classic", "Social"] |
| } |
| ]; |
| |
| // Initialize Application |
| async function init() { |
| showStatus("Loading Embedding Model (for Search)...", 0); |
| |
| try { |
| // Load Embedding Model (MiniLM for fast semantic search) |
| embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6', { |
| dtype: 'q8', // Quantized for speed |
| progress_callback: (data) => { |
| if (data.status === 'progress') { |
| showStatus(`Loading Embedding: ${data.file} (${Math.round(data.progress * 100)}%)`, data.progress * 100); |
| } |
| } |
| }); |
| |
| showStatus("Loading Summarization Model...", 30); |
| |
| // Load Summarization Model (DistilBART for speed) |
| summarizer = await pipeline('summarization', 'Xenova/distilbart-cnn-12-6', { |
| dtype: 'q8', |
| progress_callback: (data) => { |
| if (data.status === 'progress') { |
| showStatus(`Loading Summarizer: ${data.file} (${Math.round(data.progress * 100)}%)`, 30 + data.progress * 40); |
| } |
| } |
| }); |
| |
| showStatus("Loading Classification Model...", 70); |
| |
| // Load Zero-Shot Classification (BART for topic detection) |
| classifier = await pipeline('zero-shot-classification', 'Xenova/bart-base', { |
| dtype: 'q8', |
| progress_callback: (data) => { |
| if (data.status === 'progress') { |
| showStatus(`Loading Classifier: ${data.file} (${Math.round(data.progress * 100)}%)`, 70 + data.progress * 30); |
| } |
| } |
| }); |
| |
| isModelsLoaded = true; |
| hideStatus(); |
| renderLibrary(); |
| console.log("All models loaded successfully."); |
| |
| // Enable all buttons |
| document.querySelectorAll('.action-btn').forEach(btn => { |
| btn.disabled = false; |
| }); |
| |
| } catch (err) { |
| console.error(err); |
| showError("Error loading models: " + err.message); |
| } |
| } |
| |
| // UI Helpers |
| function showStatus(text, progress = 0) { |
| const container = document.getElementById('status-container'); |
| const textEl = document.getElementById('status-text'); |
| const progressFill = document.getElementById('progress-fill'); |
| container.style.display = 'flex'; |
| textEl.innerText = text; |
| progressFill.style.width = `${progress}%`; |
| } |
| |
| function hideStatus() { |
| document.getElementById('status-container').style.display = 'none'; |
| } |
| |
| function showError(message) { |
| const container = document.getElementById('status-container'); |
| container.style.display = 'flex'; |
| container.style.background = '#ffebee'; |
| container.style.color = '#c62828'; |
| const textEl = document.getElementById('status-text'); |
| textEl.innerText = message; |
| document.getElementById('progress-fill').style.width = '0%'; |
| } |
| |
| function switchTab(tabId) { |
| // Update Nav |
| document.querySelectorAll('nav button').forEach(b => b.classList.remove('active')); |
| document.getElementById(`tab-${tabId}`).classList.add('active'); |
| |
| // Update Section |
| document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); |
| document.getElementById(tabId).classList.add('active'); |
| } |
| |
| // Feature 1: Render Library |
| function renderLibrary() { |
| const grid = document.getElementById('library-grid'); |
| grid.innerHTML = ''; |
| |
| libraryData.forEach(book => { |
| const card = document.createElement('div'); |
| card.className = 'card'; |
| card.innerHTML = ` |
| <h3>${book.title}</h3> |
| <div class="meta"> |
| <span>${book.author}</span> |
| <span>${book.year}</span> |
| </div> |
| <p style="font-size: 0.9rem; color: #555; height: 60px; overflow: hidden;">${book.text}</p> |
| <div style="margin-top: 10px;"> |
| ${book.tags.map(t => `<span class="tag">${t}</span>`).join('')} |
| </div> |
| `; |
| grid.appendChild(card); |
| }); |
| } |
| |
| // Feature 2: Semantic Search |
| async function performSearch() { |
| if (!isModelsLoaded || !embedder) { |
| showSearchError("Models not loaded yet. Please wait."); |
| return; |
| } |
| |
| const query = document.getElementById('search-input').value.trim(); |
| if (!query) { |
| showSearchError("Please enter a search query."); |
| return; |
| } |
| |
| const searchBtn = document.getElementById('search-btn'); |
| searchBtn.disabled = true; |
| const resultsDiv = document.getElementById('search-results'); |
| resultsDiv.innerHTML = '<div class="spinner"></div> Searching...'; |
| |
| try { |
| // 1. Embed the query |
| const queryOutput = await embedder(query, { pooling: 'mean', normalize: true }); |
| const queryVector = queryOutput.data; // Float32Array |
| |
| // 2. Embed all library texts and calculate cosine similarity |
| const scoredBooks = libraryData.map(book => { |
| const bookOutput = await embedder(book.text, { pooling: 'mean', normalize: true }); |
| const bookVector = bookOutput.data; |
| |
| // Cosine Similarity Calculation |
| let dotProduct = 0; |
| let normQuery = 0; |
| let normBook = 0; |
| |
| for (let i = 0; i < queryVector.length; i++) { |
| dotProduct += queryVector[i] * bookVector[i]; |
| normQuery += queryVector[i] * queryVector[i]; |
| normBook += bookVector[i] * bookVector[i]; |
| } |
| const similarity = dotProduct / (Math.sqrt(normQuery) * Math.sqrt(normBook)); |
| return { ...book, similarity }; |
| }); |
| |
| // 3. Sort by similarity |
| scoredBooks.sort((a, b) => b.similarity - a.similarity); |
| |
| // 4. Render Top 3 |
| resultsDiv.innerHTML = ''; |
| const topResults = scoredBooks.slice(0, 3); |
| |
| if (topResults.length === 0) { |
| resultsDiv.innerHTML = '<p>No results found.</p>'; |
| } else { |
| topResults.forEach(result => { |
| const card = document.createElement('div'); |
| card.className = 'card'; |
| card.style.borderLeft = "4px solid #3498db"; |
| card.innerHTML = ` |
| <h3>${result.title} <span style="float:right; font-size:0.8rem">Match: ${Math.round(result.similarity * 100)}%</span></h3> |
| <div class="meta">${result.author}</div> |
| <p>${result.text}</p> |
| `; |
| resultsDiv.appendChild(card); |
| }); |
| } |
| |
| } catch (e) { |
| console.error(e); |
| showSearchError('Error performing search: ' + e.message); |
| } finally { |
| searchBtn.disabled = false; |
| } |
| } |
| |
| function showSearchError(message) { |
| const resultsDiv = document.getElementById('search-results'); |
| resultsDiv.innerHTML = `<div class="error-message">${message}</div>`; |
| } |
| |
| // Feature 3: Summarization |
| async function performSummarization() { |
| if (!isModelsLoaded || !summarizer) { |
| showSummarizeError("Models not loaded yet. Please wait."); |
| return; |
| } |
| |
| const text = document.getElementById('summarize-input').value.trim(); |
| if (!text) { |
| showSummarizeError("Please enter text to summarize."); |
| return; |
| } |
| |
| const summarizeBtn = document.getElementById('summarize-btn'); |
| summarizeBtn.disabled = true; |
| const outputDiv = document.getElementById('summarize-output'); |
| outputDiv.innerHTML = '<div class="spinner"></div> Summarizing...'; |
| |
| try { |
| const result = await summarizer(text, { |
| top_k: 1, |
| max_length: 100 |
| }); |
| |
| outputDiv.innerHTML = `<strong>Summary:</strong><br>${result[0].summary_text}`; |
| } catch (e) { |
| console.error(e); |
| showSummarizeError('Error summarizing text: ' + e.message); |
| } finally { |
| summarizeBtn.disabled = false; |
| } |
| } |
| |
| function showSummarizeError(message) { |
| const outputDiv = document.getElementById('summarize-output'); |
| outputDiv.innerHTML = `<div class="error-message">${message}</div>`; |
| } |
| |
| // Feature 4: Classification (Zero-Shot) |
| async function performClassification() { |
| if (!isModelsLoaded || !classifier) { |
| showClassifyError("Models not loaded yet. Please wait."); |
| return; |
| } |
| |
| const text = document.getElementById('classify-input').value.trim(); |
| if (!text) { |
| showClassifyError("Please enter text to classify."); |
| return; |
| } |
| |
| const classifyBtn = document.getElementById('classify-btn'); |
| classifyBtn.disabled = true; |
| const outputDiv = document.getElementById('classify-output'); |
| outputDiv.innerHTML = '<div class="spinner"></div> Analyzing topics...'; |
| |
| try { |
| // Define candidate labels for a library |
| const candidateLabels = ["Science", "History", "Fiction", "Romance", "Technology", "Biography", "Fantasy"]; |
| |
| const result = await classifier(text, candidateLabels); |
| |
| // Format output |
| let html = '<strong>Recommended Tags:</strong><br>'; |
| result.labels.forEach((label, index) => { |
| const score = Math.round(result.scores[index] * 100); |
| html += `<span class="tag" style="background:${score > 50 ? '#3498db' : '#eee'}">${label} (${score}%)</span>`; |
| }); |
| |
| outputDiv.innerHTML = html; |
| } catch (e) { |
| console.error(e); |
| showClassifyError('Error classifying text: ' + e.message); |
| } finally { |
| classifyBtn.disabled = false; |
| } |
| } |
| |
| function showClassifyError(message) { |
| const outputDiv = document.getElementById('classify-output'); |
| outputDiv.innerHTML = `<div class="error-message">${message}</div>`; |
| } |
| |
| // Start initialization |
| init(); |
| </script> |
| </body> |
|
|
| </html> |