Spaces:
Running
Running
Download index.html from aied-lab/Course_Demo: direct link, hf CLI and curl.
- Browser
- Download file 16.7 kB
-
https://huggingface.co/spaces/aied-lab/Course_Demo/resolve/main/index.html
- Command line
-
hf download hf://spaces/aied-lab/Course_Demo/index.html
-
curl -L -o index.html https://huggingface.co/spaces/aied-lab/Course_Demo/resolve/main/index.html
16.7 kB
| <html lang="zh-Hant"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Gemma 多模態影像分析助手</title> | |
| <script> | |
| (function () { | |
| var theme; | |
| try { | |
| theme = localStorage.getItem('theme'); | |
| } catch (e) {} | |
| if (!theme) { | |
| theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; | |
| } | |
| document.documentElement.dataset.theme = theme; | |
| })(); | |
| </script> | |
| <style> | |
| :root, | |
| :root[data-theme="light"] { | |
| --bg-color: #ffffff; | |
| --bg-glow-1: rgba(124, 58, 237, 0.07); | |
| --bg-glow-2: rgba(8, 145, 178, 0.07); | |
| --card-bg: rgba(255, 255, 255, 0.9); | |
| --card-border: rgba(15, 23, 42, 0.08); | |
| --border-glow: rgba(8, 145, 178, 0.35); | |
| --neon-cyan: #0891b2; | |
| --neon-purple: #7c3aed; | |
| --text-main: #0f172a; | |
| --text-muted: #64748b; | |
| --input-bg: #f8fafc; | |
| --input-border: rgba(15, 23, 42, 0.12); | |
| --result-bg: #f8fafc; | |
| --result-text: #1e293b; | |
| --result-shadow: inset 0 1px 3px rgba(15, 23, 42, 0.06); | |
| --card-shadow: 0 10px 30px rgba(15, 23, 42, 0.08), inset 0 0 15px rgba(124, 58, 237, 0.03); | |
| --preview-shadow: 0 4px 12px rgba(15, 23, 42, 0.15); | |
| --focus-shadow: rgba(8, 145, 178, 0.25); | |
| --btn-text: #ffffff; | |
| --btn-disabled-bg: #e2e8f0; | |
| --btn-disabled-text: #94a3b8; | |
| --scanline-opacity: 0.6; | |
| } | |
| :root[data-theme="dark"] { | |
| --bg-color: #0b0f19; | |
| --bg-glow-1: rgba(176, 38, 255, 0.1); | |
| --bg-glow-2: rgba(0, 240, 255, 0.1); | |
| --card-bg: rgba(17, 24, 39, 0.75); | |
| --card-border: rgba(0, 240, 255, 0.3); | |
| --border-glow: rgba(0, 240, 255, 0.3); | |
| --neon-cyan: #00f0ff; | |
| --neon-purple: #b026ff; | |
| --text-main: #f3f4f6; | |
| --text-muted: #9ca3af; | |
| --input-bg: rgba(3, 7, 18, 0.6); | |
| --input-border: rgba(255, 255, 255, 0.1); | |
| --result-bg: rgba(3, 7, 18, 0.8); | |
| --result-text: #e5e7eb; | |
| --result-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4); | |
| --card-shadow: 0 0 30px rgba(0, 240, 255, 0.1), inset 0 0 15px rgba(176, 38, 255, 0.05); | |
| --preview-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); | |
| --focus-shadow: rgba(0, 240, 255, 0.3); | |
| --btn-text: #000000; | |
| --btn-disabled-bg: #374151; | |
| --btn-disabled-text: #9ca3af; | |
| --scanline-opacity: 1; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| max-width: 650px; | |
| margin: 40px auto; | |
| padding: 20px; | |
| background-color: var(--bg-color); | |
| background-image: | |
| radial-gradient(circle at 10% 20%, var(--bg-glow-1) 0%, transparent 40%), | |
| radial-gradient(circle at 90% 80%, var(--bg-glow-2) 0%, transparent 40%); | |
| color: var(--text-main); | |
| min-height: 100vh; | |
| transition: background-color 0.35s ease, color 0.35s ease; | |
| } | |
| .card { | |
| background: var(--card-bg); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| padding: 30px; | |
| border-radius: 16px; | |
| border: 1px solid var(--card-border); | |
| box-shadow: var(--card-shadow); | |
| position: relative; | |
| overflow: hidden; | |
| transition: background-color 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease; | |
| } | |
| .card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 2px; | |
| background: linear-gradient(90deg, transparent, var(--neon-cyan), transparent); | |
| opacity: var(--scanline-opacity); | |
| animation: scanline 4s linear infinite; | |
| } | |
| @keyframes scanline { | |
| 0% { left: -100%; } | |
| 100% { left: 100%; } | |
| } | |
| h2 { | |
| margin-top: 0; | |
| font-size: 1.8rem; | |
| background: linear-gradient(135deg, var(--neon-cyan), var(--neon-purple)); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| letter-spacing: 0.5px; | |
| } | |
| p { | |
| color: var(--text-muted); | |
| font-size: 0.95rem; | |
| margin-bottom: 24px; | |
| } | |
| .form-group { | |
| margin-bottom: 20px; | |
| } | |
| label { | |
| display: block; | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| font-size: 0.9rem; | |
| color: var(--neon-cyan); | |
| letter-spacing: 0.5px; | |
| } | |
| input[type="password"], | |
| input[type="text"], | |
| input[type="file"], | |
| textarea { | |
| width: 100%; | |
| padding: 12px 14px; | |
| background: var(--input-bg); | |
| border: 1px solid var(--input-border); | |
| border-radius: 8px; | |
| box-sizing: border-box; | |
| color: var(--text-main); | |
| font-size: 0.95rem; | |
| transition: background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| input:focus { | |
| outline: none; | |
| border-color: var(--neon-cyan); | |
| box-shadow: 0 0 10px var(--focus-shadow); | |
| } | |
| input[type="file"]::file-selector-button { | |
| background: color-mix(in srgb, var(--neon-cyan) 12%, transparent); | |
| color: var(--neon-cyan); | |
| border: 1px solid var(--neon-cyan); | |
| padding: 6px 12px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| input[type="file"]::file-selector-button:hover { | |
| background: var(--neon-cyan); | |
| color: var(--btn-text); | |
| } | |
| button { | |
| background: linear-gradient(135deg, var(--neon-cyan), var(--neon-purple)); | |
| color: var(--btn-text); | |
| border: none; | |
| padding: 12px 20px; | |
| font-weight: 700; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| width: 100%; | |
| font-size: 1rem; | |
| letter-spacing: 1px; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(0, 240, 255, 0.2); | |
| } | |
| button:hover { | |
| opacity: 0.9; | |
| transform: translateY(-1px); | |
| box-shadow: 0 6px 20px rgba(176, 38, 255, 0.4); | |
| } | |
| button:disabled { | |
| background: var(--btn-disabled-bg); | |
| color: var(--btn-disabled-text); | |
| cursor: not-allowed; | |
| box-shadow: none; | |
| transform: none; | |
| } | |
| .theme-toggle { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| z-index: 10; | |
| width: auto; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 8px 14px; | |
| border-radius: 999px; | |
| border: 1px solid var(--input-border); | |
| background: var(--card-bg); | |
| color: var(--text-muted); | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| letter-spacing: 0; | |
| box-shadow: 0 4px 14px rgba(15, 23, 42, 0.12); | |
| backdrop-filter: blur(8px); | |
| -webkit-backdrop-filter: blur(8px); | |
| } | |
| .theme-toggle:hover { | |
| opacity: 1; | |
| transform: none; | |
| color: var(--text-main); | |
| box-shadow: 0 4px 14px rgba(15, 23, 42, 0.18); | |
| } | |
| .theme-toggle svg { | |
| width: 16px; | |
| height: 16px; | |
| flex-shrink: 0; | |
| } | |
| :root[data-theme="light"] .icon-sun, | |
| :root[data-theme="dark"] .icon-moon { | |
| display: none; | |
| } | |
| #preview-container { | |
| margin-top: 12px; | |
| text-align: center; | |
| } | |
| #imagePreview { | |
| max-width: 100%; | |
| max-height: 250px; | |
| border-radius: 8px; | |
| display: none; | |
| border: 1px solid var(--border-glow); | |
| box-shadow: var(--preview-shadow); | |
| } | |
| #result { | |
| margin-top: 12px; | |
| padding: 16px; | |
| background: var(--result-bg); | |
| border: 1px solid var(--input-border); | |
| border-radius: 8px; | |
| white-space: pre-wrap; | |
| min-height: 60px; | |
| font-size: 0.95rem; | |
| color: var(--result-text); | |
| box-shadow: var(--result-shadow); | |
| transition: background-color 0.35s ease, color 0.35s ease, border-color 0.35s ease; | |
| } | |
| @media (max-width: 520px) { | |
| body { | |
| margin: 20px auto; | |
| padding: 16px; | |
| } | |
| .theme-toggle { | |
| top: 12px; | |
| right: 12px; | |
| } | |
| .card { | |
| padding: 22px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <button class="theme-toggle" id="themeToggle" type="button" aria-label="切換背景樣式" onclick="toggleTheme()"> | |
| <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"> | |
| <circle cx="12" cy="12" r="4"></circle> | |
| <path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"></path> | |
| </svg> | |
| <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> | |
| <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"></path> | |
| </svg> | |
| <span id="themeLabel">深色</span> | |
| </button> | |
| <div class="card"> | |
| <h2>Gemma 多模態影像分析助手</h2> | |
| <p>透過 Hugging Face Router 呼叫 gemma-4-31B-it 模型進行高精度影像辨識與分析。</p> | |
| <div class="form-group"> | |
| <label for="apiKey">Hugging Face Token (HF_TOKEN)</label> | |
| <input type="password" id="apiKey" placeholder="請輸入您的 HF_TOKEN"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="imageInput">上傳圖片</label> | |
| <input type="file" id="imageInput" accept="image/*" onchange="previewSelectedImage(event)"> | |
| <div id="preview-container"> | |
| <img id="imagePreview" alt="圖片預覽"> | |
| </div> | |
| </div> | |
| <div class="form-group"> | |
| <label for="promptInput">輸入提示詞 (Prompt)</label> | |
| <input type="text" id="promptInput" value="Describe this image in one sentence in chinese"> | |
| </div> | |
| <button id="submitBtn" onclick="analyzeImage()">啟動分析</button> | |
| <div class="form-group" style="margin-top: 24px;"> | |
| <label>模型回應</label> | |
| <div id="result">系統待命,尚未開始分析...</div> | |
| </div> | |
| </div> | |
| <script> | |
| function toggleTheme() { | |
| const root = document.documentElement; | |
| const next = root.dataset.theme === 'dark' ? 'light' : 'dark'; | |
| root.dataset.theme = next; | |
| try { | |
| localStorage.setItem('theme', next); | |
| } catch (e) {} | |
| updateThemeLabel(); | |
| } | |
| function updateThemeLabel() { | |
| const isDark = document.documentElement.dataset.theme === 'dark'; | |
| document.getElementById('themeLabel').textContent = isDark ? '淺色' : '深色'; | |
| document.getElementById('themeToggle').setAttribute('aria-label', isDark ? '切換為淺色背景' : '切換為深色背景'); | |
| } | |
| updateThemeLabel(); | |
| // 選擇圖片時即時顯示預覽 | |
| function previewSelectedImage(event) { | |
| const file = event.target.files[0]; | |
| const preview = document.getElementById('imagePreview'); | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| preview.src = e.target.result; | |
| preview.style.display = 'inline-block'; | |
| } | |
| reader.readAsDataURL(file); | |
| } else { | |
| preview.src = ''; | |
| preview.style.display = 'none'; | |
| } | |
| } | |
| // 壓縮並轉成 Base64 的前端實作 | |
| function compressImage(file, maxSize = 1024, quality = 0.85) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| let width = img.width; | |
| let height = img.height; | |
| if (width > height) { | |
| if (width > maxSize) { | |
| height *= maxSize / width; | |
| width = maxSize; | |
| } | |
| } else { | |
| if (height > maxSize) { | |
| width *= maxSize / height; | |
| height = maxSize; | |
| } | |
| } | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = width; | |
| canvas.height = height; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(img, 0, 0, width, height); | |
| const dataUrl = canvas.toDataURL('image/jpeg', quality); | |
| resolve(dataUrl); | |
| }; | |
| img.src = e.target.result; | |
| }; | |
| reader.onerror = reject; | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| async function analyzeImage() { | |
| const apiKey = document.getElementById('apiKey').value.trim(); | |
| const imageFile = document.getElementById('imageInput').files[0]; | |
| let prompt = document.getElementById('promptInput').value.trim(); | |
| const resultDiv = document.getElementById('result'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| if (!apiKey) { | |
| alert('請先輸入 Hugging Face Token!'); | |
| return; | |
| } | |
| if (!imageFile) { | |
| alert('請先上傳一張圖片!'); | |
| return; | |
| } | |
| if (!prompt) { | |
| prompt = "Describe this image in one sentence."; | |
| } | |
| submitBtn.disabled = true; | |
| resultDiv.innerText = '處理中,神經網路運算中...'; | |
| try { | |
| const base64Image = await compressImage(imageFile); | |
| const response = await fetch("https://router.huggingface.co/v1/chat/completions", { | |
| method: "POST", | |
| headers: { | |
| "Authorization": `Bearer ${apiKey}`, | |
| "Content-Type": "application/json" | |
| }, | |
| body: JSON.stringify({ | |
| model: "google/gemma-4-31B-it:novita", | |
| messages: [{ | |
| role: "user", | |
| content: [ | |
| { type: "text", text: prompt }, | |
| { type: "image_url", image_url: { url: base64Image } } | |
| ] | |
| }], | |
| max_tokens: 300 | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (response.ok && data.choices && data.choices.length > 0) { | |
| resultDiv.innerText = data.choices[0].message.content; | |
| } else { | |
| resultDiv.innerText = `發生錯誤:${data.error?.message || JSON.stringify(data)}`; | |
| } | |
| } catch (error) { | |
| resultDiv.innerText = `發生錯誤:${error.message}`; | |
| } finally { | |
| submitBtn.disabled = false; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |