// Application Entry State let currentPptId = "ppt1"; let currentSlideIndex = 0; // 0-indexed index inside the selected PPT's slides list let searchIndex = []; // DOM Elements const menuItems = document.querySelectorAll(".menu-item"); const tabSections = document.querySelectorAll(".tab-section"); const tabTitleEl = document.getElementById("current-tab-title"); const tabSubtitleEl = document.getElementById("current-tab-subtitle"); const pptSelectEl = document.getElementById("ppt-select"); const slideThumbnailsEl = document.getElementById("slide-thumbnails"); const mainSlideImgEl = document.getElementById("main-slide-img"); const slideIndexIndicatorEl = document.getElementById("slide-index-indicator"); const btnPrevSlide = document.getElementById("btn-prev-slide"); const btnNextSlide = document.getElementById("btn-next-slide"); const slideTextLinesEl = document.getElementById("slide-text-lines"); const slideTakeawayEl = document.getElementById("slide-takeaway-points"); const btnFullscreen = document.getElementById("btn-fullscreen"); const lightroomEl = document.getElementById("lightroom"); const lightroomImgEl = document.getElementById("lightroom-img"); const btnCloseLightroom = document.getElementById("btn-close-lightroom"); // Calculator inputs const sliderDepth = document.getElementById("slider-depth"); const sliderDist = document.getElementById("slider-dist"); const sliderVs = document.getElementById("slider-vs"); const sliderProcess = document.getElementById("slider-process"); const valDepth = document.getElementById("depth-val"); const valDist = document.getElementById("dist-val"); const valVs = document.getElementById("vs-val"); const valProcess = document.getElementById("process-val"); const calcLeadTimeEl = document.getElementById("calc-lead-time"); const calcWarningStatusEl = document.getElementById("calc-warning-status"); const txtBlindRadius = document.getElementById("blind-radius-txt"); const txtHypoDist = document.getElementById("hypo-dist-txt"); const blindZoneCanvas = document.getElementById("blind-zone-canvas"); // Search inputs const searchInput = document.getElementById("search-input"); const searchResults = document.getElementById("search-results"); // Tab configuration const tabMeta = { overview: { title: "知識庫總覽", subtitle: "展示台灣中央氣象署強震即時警報與 AI 技術整合之演進" }, highlights: { title: "簡報精選精華", subtitle: "彙整四大簡報最關鍵的核心圖表與重要原理,支援一鍵定位簡報脈絡" }, viewer: { title: "投影片展示", subtitle: "逐頁瀏覽 4 份核心簡報精華、提取文字與高畫質圖片" }, calculator: { title: "預警盲區模擬", subtitle: "互動式物理參數計算,深入理解應變盲區與警報前置時間的幾何關係" }, swarm: { title: "AI 代理協作", subtitle: "解析 2026 年 CWA 強震即時警報系統中多代理群組自動化閉環工作流" }, lem: { title: "大型地震模型 (LEM)", subtitle: "介紹以 Wav2Vec 2.0 為基座演化的次世代地震自監督深度學習模型 SeisWav2Vec" }, search: { title: "全文大綱搜尋", subtitle: "高效檢索四大簡報共 164 頁的文字內容,快速定位知識點" }, downloads: { title: "簡報下載專區", subtitle: "查閱本地原始 PPTX 簡報大小與詳細檔案路徑資訊" } }; // Initialize Application window.addEventListener("DOMContentLoaded", () => { initNavigation(); initSlideViewer(); initCalculator(); initSwarmVisualizer(); initSearchEngine(); initLightroom(); initHighlights(); }); // 1. Navigation Controller function initNavigation() { menuItems.forEach(item => { item.addEventListener("click", () => { const tabId = item.getAttribute("data-tab"); // Update sidebar state menuItems.forEach(mi => mi.classList.remove("active")); item.classList.add("active"); // Update viewport state tabSections.forEach(section => { section.classList.remove("active"); if (section.id === `tab-${tabId}`) { section.classList.add("active"); } }); // Update header titles const meta = tabMeta[tabId] || { title: "展示平台", subtitle: "" }; tabTitleEl.textContent = meta.title; tabSubtitleEl.textContent = meta.subtitle; // Special canvas re-draw on tab visibility if (tabId === "calculator") { setTimeout(drawBlindZone, 100); } else if (tabId === "swarm") { setTimeout(drawSwarm, 100); } }); }); } function jumpToTab(tabId) { const item = document.querySelector(`.menu-item[data-tab="${tabId}"]`); if (item) item.click(); } function jumpToPresentation(pptId, slideNum = 1) { currentPptId = pptId; pptSelectEl.value = pptId; currentSlideIndex = slideNum - 1; renderSlidesList(); jumpToTab("viewer"); } // 2. Slide Showcase Controller function initSlideViewer() { pptSelectEl.addEventListener("change", (e) => { currentPptId = e.target.value; currentSlideIndex = 0; renderSlidesList(); }); btnPrevSlide.addEventListener("click", () => { if (currentSlideIndex > 0) { currentSlideIndex--; updateActiveSlide(); } }); btnNextSlide.addEventListener("click", () => { const pptData = slidesData.find(p => p.id === currentPptId); if (pptData && currentSlideIndex < pptData.slides.length - 1) { currentSlideIndex++; updateActiveSlide(); } }); renderSlidesList(); } function renderSlidesList() { const pptData = slidesData.find(p => p.id === currentPptId); if (!pptData) return; slideThumbnailsEl.innerHTML = ""; pptData.slides.forEach((slide, idx) => { const thumb = document.createElement("div"); thumb.className = `thumb-item ${idx === currentSlideIndex ? 'active' : ''}`; thumb.setAttribute("data-index", idx); // Find slide title or summary let titleStr = slide.title || (slide.text && slide.text[0]) || `Slide ${slide.slideNumber}`; if (titleStr.length > 22) { titleStr = titleStr.substring(0, 20) + "..."; } thumb.innerHTML = ` Slide ${slide.slideNumber} ${titleStr} `; thumb.addEventListener("click", () => { currentSlideIndex = idx; updateActiveSlide(); }); slideThumbnailsEl.appendChild(thumb); }); updateActiveSlide(); } function updateActiveSlide() { const pptData = slidesData.find(p => p.id === currentPptId); if (!pptData) return; const slide = pptData.slides[currentSlideIndex]; if (!slide) return; // Update main display image mainSlideImgEl.src = slide.image; // Highlight active thumbnail and scroll into view smoothly const thumbs = slideThumbnailsEl.querySelectorAll(".thumb-item"); thumbs.forEach((t, idx) => { t.classList.remove("active"); if (idx === currentSlideIndex) { t.classList.add("active"); t.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }); // Update indicators slideIndexIndicatorEl.textContent = `Slide ${slide.slideNumber} / ${pptData.slidesCount}`; // Update disabled buttons btnPrevSlide.disabled = currentSlideIndex === 0; btnNextSlide.disabled = currentSlideIndex === pptData.slides.length - 1; // Render slide texts slideTextLinesEl.innerHTML = ""; if (slide.text && slide.text.length > 0) { slide.text.forEach(line => { const li = document.createElement("li"); li.textContent = line; slideTextLinesEl.appendChild(li); }); } else { const li = document.createElement("li"); li.textContent = "(此頁投影片無主要大綱文字,可能為全圖表展示)"; li.style.color = "var(--text-dark)"; li.style.fontStyle = "italic"; slideTextLinesEl.appendChild(li); } // Generate key terms or takeaways dynamically based on slide content slideTakeawayEl.innerHTML = ""; const takeaways = generateTakeaways(slide, pptData.id); takeaways.forEach(p => { const li = document.createElement("li"); li.textContent = p; slideTakeawayEl.appendChild(li); }); } function generateTakeaways(slide, pptId) { const points = []; const textJoined = (slide.text || []).join(" ").toLowerCase(); const title = (slide.title || "").toLowerCase(); if (pptId === "ppt1") { if (textJoined.includes("earthworm")) points.push("Earthworm:USGS 開源地震處理模組化系統,為全球實時監控基準。"); if (textJoined.includes("seiscomp")) points.push("SeisComP:先進地震圖形界面與定位波相拾取平台。"); if (textJoined.includes("grafana")) points.push("Grafana 觀測:即時視覺化海底觀測網水壓計、波形延遲及預警推播狀態。"); if (textJoined.includes("ebear")) points.push("eBEAR:CWA 自行開發的 Earthworm Alert Reporting 自動速報系統。"); if (textJoined.includes("docker")) points.push("容器化部署:CWA EEW 與 Seiscomp 已打包為 Docker 映像檔上傳 Docker Hub。"); } else if (pptId === "ppt2") { if (textJoined.includes("pws")) points.push("PWS:災防告警細胞廣播發布範疇統計(如 M6.1 地震第一報與第二報範圍)。"); if (textJoined.includes("幾何中心")) points.push("定位法:幾何中心法在早期預警解算中對震央與震度有誤差修正之探討。"); if (textJoined.includes("盲區") || textJoined.includes("應變")) points.push("應變盲區:最快解算耗時乘以 S 波平均波速(3.5 km/s),該範圍內無警報前置時間。"); if (textJoined.includes("觸發")) points.push("測站觸發率:針對特定震源以半徑搜索,排除共站干擾計算實時觸發比例。"); } else if (pptId === "ppt3") { if (textJoined.includes("agent") || textJoined.includes("代理")) points.push("AI Agent Swarm:my_agent, seismo, seismo_agent, secondary_agent 四協同代理職責分工。"); if (textJoined.includes("wav2vec") || textJoined.includes("lem")) points.push("SeisWav2Vec 2.0:對比學習 Wav2Vec 結構遷移至地震波形,自監督預訓練模型。"); if (textJoined.includes("transformer")) points.push("Transformer 上下文:12 層注意力機制學習特徵的全局上下文,無標記資料自學習。"); if (textJoined.includes("telegram")) points.push("Telegram 通訊:整合 my_agent 自動推送 eew_health、異常日誌及 daily_report。"); } else if (pptId === "ppt4") { if (textJoined.includes("p波") || textJoined.includes("s波")) points.push("強震預警原理:利用傳播快速的 P 縱波推算震級,趕在破壞性 S 橫波抵達前告警。"); if (textJoined.includes("電視") || textJoined.includes("推播")) points.push("多元傳播管道:國家級警報、電視台即時插播、中小學預警軟體及手機 APP 四大網絡。"); if (textJoined.includes("花蓮")) points.push("花蓮 M7.2 案例:回顧強震預警警報實際發送實例、各縣市震度與警報時效。"); } if (points.length === 0) { points.push("地震預警核心知識:解析地震波傳播時序與即時資料串流。"); points.push("技術重點:結合自動化解算模型,縮減預警盲區以求減災。"); } return points; } // 3. Lightroom Lightbox Modal function initLightroom() { mainSlideImgEl.addEventListener("click", () => { lightroomImgEl.src = mainSlideImgEl.src; lightroomEl.style.display = "flex"; }); lightroomEl.addEventListener("click", () => { lightroomEl.style.display = "none"; }); btnCloseLightroom.addEventListener("click", (e) => { e.stopPropagation(); lightroomEl.style.display = "none"; }); } // 4. Blind Zone & Lead Time Warning Simulator function initCalculator() { const updateCalculator = () => { const depth = parseFloat(sliderDepth.value); const dist = parseFloat(sliderDist.value); const vs = parseFloat(sliderVs.value); const processTime = parseFloat(sliderProcess.value); // Display value update valDepth.textContent = `${depth} km`; valDist.textContent = `${dist} km`; valVs.textContent = `${vs.toFixed(1)} km/s`; valProcess.textContent = `${processTime.toFixed(1)} s`; // Geometry math const hypoDist = Math.sqrt(dist*dist + depth*depth); const sTravelTime = hypoDist / vs; const leadTime = sTravelTime - processTime; // Blind zone radius (ground distance when S-wave wavefront intersects ground surface at the processing time) // S-wave path distance at processing time: s_path = processTime * vs // If path is less than focal depth, S-wave hasn't even reached the ground surface directly above epicenter. let blindRadius = 0; const sPath = processTime * vs; if (sPath > depth) { blindRadius = Math.sqrt(sPath*sPath - depth*depth); } // Render text txtHypoDist.textContent = `${hypoDist.toFixed(1)} km`; txtBlindRadius.textContent = blindRadius > 0 ? `${blindRadius.toFixed(1)} km` : "0 km (未傳抵地表)"; if (leadTime > 0) { calcLeadTimeEl.innerHTML = `${leadTime.toFixed(1)}秒`; calcLeadTimeEl.style.color = "var(--accent-cyan)"; calcWarningStatusEl.textContent = "警報覆蓋安全區"; calcWarningStatusEl.className = "warning-status-alert status-safe"; } else { calcLeadTimeEl.innerHTML = `0.0秒`; calcLeadTimeEl.style.color = "var(--accent-rose)"; calcWarningStatusEl.textContent = "處於預警盲區內"; calcWarningStatusEl.className = "warning-status-alert status-blind"; } // Save to window state for canvas redraw window.calcState = { depth, dist, vs, processTime, hypoDist, blindRadius, leadTime }; drawBlindZone(); }; sliderDepth.addEventListener("input", updateCalculator); sliderDist.addEventListener("input", updateCalculator); sliderVs.addEventListener("input", updateCalculator); sliderProcess.addEventListener("input", updateCalculator); updateCalculator(); } function drawBlindZone() { const canvas = blindZoneCanvas; if (!canvas) return; const ctx = canvas.getContext("2d"); // Set resolution based on DPI const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.scale(dpr, dpr); const w = rect.width; const h = rect.height; ctx.clearRect(0, 0, w, h); const state = window.calcState; if (!state) return; // Drawing coordinates mapping // Earth surface is a horizontal line at y = 100 const ySurface = 80; // Center is epicenter at x = w / 2 - 100 const xEpicenter = w / 2 - 80; // Scale pixels: 1 km = 2 pixels const scale = 2.0; // Draw Earth crust layer ctx.fillStyle = "rgba(10, 15, 26, 0.4)"; ctx.fillRect(0, ySurface, w, h - ySurface); // Draw ground line ctx.strokeStyle = "#475569"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, ySurface); ctx.lineTo(w, ySurface); ctx.stroke(); // Epicenter ctx.fillStyle = "#94a3b8"; ctx.beginPath(); ctx.arc(xEpicenter, ySurface, 4, 0, 2*Math.PI); ctx.fill(); // Hypocenter (Earthquake Focus) const yFocus = ySurface + state.depth * scale; ctx.fillStyle = "var(--accent-rose)"; ctx.beginPath(); ctx.arc(xEpicenter, yFocus, 6, 0, 2*Math.PI); ctx.fill(); // Glow for Hypocenter const gradientFocus = ctx.createRadialGradient(xEpicenter, yFocus, 2, xEpicenter, yFocus, 18); gradientFocus.addColorStop(0, "rgba(244, 63, 94, 0.6)"); gradientFocus.addColorStop(1, "rgba(244, 63, 94, 0)"); ctx.fillStyle = gradientFocus; ctx.beginPath(); ctx.arc(xEpicenter, yFocus, 18, 0, 2*Math.PI); ctx.fill(); // Target Station (user epicenter distance) const xStation = xEpicenter + state.dist * scale; ctx.fillStyle = state.leadTime > 0 ? "var(--accent-cyan)" : "var(--accent-rose)"; ctx.beginPath(); ctx.arc(xStation, ySurface, 5, 0, 2*Math.PI); ctx.fill(); // Label for target ctx.fillStyle = "white"; ctx.font = "bold 11px sans-serif"; ctx.fillText("目標觀測點", xStation - 25, ySurface - 12); // Wavefront propagation circle // S-wave wavefront path distance const rSwave = state.processTime * state.vs * scale; ctx.strokeStyle = "rgba(245, 158, 11, 0.4)"; ctx.lineWidth = 1.5; ctx.setLineDash([4, 3]); ctx.beginPath(); ctx.arc(xEpicenter, yFocus, rSwave, 0, 2*Math.PI); ctx.stroke(); ctx.setLineDash([]); // P-wave wavefront path (approx 1.73x faster than S-wave) const rPwave = state.processTime * state.vs * 1.73 * scale; ctx.strokeStyle = "rgba(6, 182, 212, 0.25)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(xEpicenter, yFocus, rPwave, 0, 2*Math.PI); ctx.stroke(); // Shaded Blind Zone on surface const rBlindPx = state.blindRadius * scale; if (rBlindPx > 0) { ctx.fillStyle = "rgba(244, 63, 94, 0.15)"; ctx.fillRect(xEpicenter - rBlindPx, ySurface, rBlindPx * 2, h - ySurface); // Draw surface indicator for blind zone ctx.strokeStyle = "var(--accent-rose)"; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(xEpicenter - rBlindPx, ySurface); ctx.lineTo(xEpicenter + rBlindPx, ySurface); ctx.stroke(); // Blind Zone Label ctx.fillStyle = "var(--accent-rose)"; ctx.font = "bold 11px sans-serif"; ctx.fillText(`預警盲區半徑: ${state.blindRadius.toFixed(1)} km`, xEpicenter - 65, ySurface + 24); } // S-wave wavefront intersection with ground ctx.fillStyle = "var(--accent-amber)"; const intersectDist = state.processTime * state.vs; if (intersectDist > state.depth) { const xIntersectLeft = xEpicenter - rBlindPx; const xIntersectRight = xEpicenter + rBlindPx; ctx.beginPath(); ctx.arc(xIntersectLeft, ySurface, 4, 0, 2*Math.PI); ctx.arc(xIntersectRight, ySurface, 4, 0, 2*Math.PI); ctx.fill(); } // Draw ray from Hypocenter to target station ctx.strokeStyle = "rgba(255, 255, 255, 0.15)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(xEpicenter, yFocus); ctx.lineTo(xStation, ySurface); ctx.stroke(); // Annotations ctx.fillStyle = "var(--text-dark)"; ctx.font = "10px sans-serif"; ctx.fillText("震央 (Epicenter)", xEpicenter + 8, ySurface - 12); ctx.fillStyle = "var(--accent-rose)"; ctx.fillText(`震源 (Hypocenter) Depth: ${state.depth} km`, xEpicenter + 12, yFocus + 4); } // 5. AI Agent Swarm network visualizer function initSwarmVisualizer() { const swarmCanvas = document.getElementById("agent-swarm-canvas"); if (!swarmCanvas) return; // Node details const agentNodes = [ { id: "my_agent", name: "my_agent", role: "預警總管", color: "var(--accent-emerald)", bg: "var(--accent-emerald-glow)", x: 200, y: 120, r: 48, desc: "主控監測流程,定時健康檢查 (eew_health cron)。判讀並處理異常事件,異常時透過 Telegram 主動發送告警,並彙整所有 agent 資訊產出每日簡報日報 (eew_report_daily)。" }, { id: "seismo", name: "seismo", role: "數據前向轉發", color: "var(--accent-blue)", bg: "var(--accent-blue-glow)", x: 450, y: 120, r: 42, desc: "維持持續運行的心跳循環 (heartbeat loop),即時偵測及提取中央氣象署 EEW 流程產生的最新地震 `.rep` 報告原始檔,將資料進行結構化轉換輸出 (轉為 JSON/CSV)。" }, { id: "seismo_agent", name: "seismo_agent", role: "地震學分析助教", color: "var(--accent-amber)", bg: "var(--accent-amber-glow)", x: 450, y: 320, r: 42, desc: "負責科學數據分析與處理流水線。解析速報效能與測站延遲統計,呼叫自動繪圖模組繪製事件散佈圖與序列圖,同時儲存備份二進位原始數據檔。" }, { id: "secondary_agent", name: "secondary_agent", role: "系統整合工程師", color: "var(--accent-rose)", bg: "var(--accent-rose-glow)", x: 200, y: 320, r: 48, desc: "本地調度與訊息轉發中心。負責部署與維護容器重啟腳本 (eew_fault_recovery.sh)、Cron 排程及 Telegram 機器人核心,負責在 my_agent 要求時調用 delegate_task 指派任務。" } ]; const links = [ { from: "seismo", to: "secondary_agent", label: "地震.rep JSON 輸出" }, { from: "secondary_agent", to: "seismo_agent", label: "分發科研分析與繪圖" }, { from: "seismo_agent", to: "my_agent", label: "回傳延遲時效日報" }, { from: "my_agent", to: "secondary_agent", label: "調度容器故障重啟" }, { from: "seismo", to: "my_agent", label: "心跳狀態報告" } ]; let selectedNodeId = "my_agent"; const updateSwarmOverlay = (node) => { document.getElementById("agent-overlay-name").textContent = node.name; document.getElementById("agent-overlay-role").textContent = node.role; document.getElementById("agent-overlay-role").style.backgroundColor = node.bg; document.getElementById("agent-overlay-role").style.color = node.color; document.getElementById("agent-overlay-desc").textContent = node.desc; document.getElementById("agent-overlay-icon").style.color = node.color; document.getElementById("agent-overlay-icon").style.backgroundColor = node.bg; }; window.drawSwarm = () => { const dpr = window.devicePixelRatio || 1; const rect = swarmCanvas.getBoundingClientRect(); swarmCanvas.width = rect.width * dpr; swarmCanvas.height = rect.height * dpr; const ctx = swarmCanvas.getContext("2d"); ctx.scale(dpr, dpr); const w = rect.width; const h = rect.height; ctx.clearRect(0, 0, w, h); // Set node positions dynamically centered to canvas const xCenter = w / 2; const yCenter = h / 2 - 40; // Re-adjust node coordinates relative to canvas center agentNodes[0].x = xCenter - 150; agentNodes[0].y = yCenter - 80; agentNodes[1].x = xCenter + 150; agentNodes[1].y = yCenter - 80; agentNodes[2].x = xCenter + 150; agentNodes[2].y = yCenter + 120; agentNodes[3].x = xCenter - 150; agentNodes[3].y = yCenter + 120; // Draw Links links.forEach(l => { const fromNode = agentNodes.find(n => n.id === l.from); const toNode = agentNodes.find(n => n.id === l.to); if (!fromNode || !toNode) return; ctx.strokeStyle = "rgba(148, 163, 184, 0.15)"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(fromNode.x, fromNode.y); ctx.lineTo(toNode.x, toNode.y); ctx.stroke(); // Draw dynamic animated pulse dot on links const speed = 0.001; const t = (Date.now() * speed) % 1; const dotX = fromNode.x + (toNode.x - fromNode.x) * t; const dotY = fromNode.y + (toNode.y - fromNode.y) * t; ctx.fillStyle = "var(--accent-cyan)"; ctx.beginPath(); ctx.arc(dotX, dotY, 4, 0, 2*Math.PI); ctx.fill(); }); // Draw Nodes agentNodes.forEach(node => { const isSelected = node.id === selectedNodeId; // Glowing shadow if (isSelected) { ctx.shadowColor = node.color; ctx.shadowBlur = 15; } // Node fill ctx.fillStyle = "rgba(10, 15, 26, 0.9)"; ctx.strokeStyle = isSelected ? "white" : node.color; ctx.lineWidth = isSelected ? 3 : 1.5; ctx.beginPath(); ctx.arc(node.x, node.y, node.r, 0, 2*Math.PI); ctx.fill(); ctx.stroke(); // Reset shadow ctx.shadowBlur = 0; // Text ctx.fillStyle = "white"; ctx.font = "bold 12px sans-serif"; ctx.textAlign = "center"; ctx.fillText(node.name, node.x, node.y - 4); ctx.fillStyle = node.color; ctx.font = "10px sans-serif"; ctx.fillText(node.role, node.x, node.y + 14); }); }; // Click handler swarmCanvas.addEventListener("click", (e) => { const rect = swarmCanvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Check if clicked node for (let node of agentNodes) { const dist = Math.sqrt((x - node.x)**2 + (y - node.y)**2); if (dist <= node.r) { selectedNodeId = node.id; updateSwarmOverlay(node); drawSwarm(); break; } } }); // Animation loop for swarm let animId; const loop = () => { if (document.getElementById("tab-swarm").classList.contains("active")) { drawSwarm(); } animId = requestAnimationFrame(loop); }; loop(); // Select default node updateSwarmOverlay(agentNodes[0]); } // 6. Search Engine Controller function initSearchEngine() { // Build slide index searchIndex = []; slidesData.forEach(ppt => { ppt.slides.forEach(slide => { searchIndex.push({ pptId: ppt.id, pptTitle: ppt.title, slideNumber: slide.slideNumber, title: slide.title || "", textLines: slide.text || [], image: slide.image }); }); }); searchInput.addEventListener("input", (e) => { const query = e.target.value.toLowerCase().trim(); if (!query) { searchResults.innerHTML = `
請輸入關鍵字以檢索四大簡報共 164 頁的文字大綱
未找到任何相符的投影片內容
請試著搜尋其他字眼(如:Earthworm, Grafana, 盲區, 代理)
${h.description}