Course_Demo / index.html
aied-lab's picture
Add light theme with dark mode toggle
60df2b7
Raw History Blame Contribute Delete
16.7 kB
<!DOCTYPE html>
<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>