// 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 頁的文字大綱

`; return; } const matches = searchIndex.filter(item => { const matchTitle = item.title.toLowerCase().includes(query); const matchPptTitle = item.pptTitle.toLowerCase().includes(query); const matchLines = item.textLines.some(line => line.toLowerCase().includes(query)); return matchTitle || matchPptTitle || matchLines; }); if (matches.length === 0) { searchResults.innerHTML = `

未找到任何相符的投影片內容

請試著搜尋其他字眼(如:Earthworm, Grafana, 盲區, 代理)

`; return; } searchResults.innerHTML = ""; matches.forEach(item => { const card = document.createElement("div"); card.className = "glass-panel card-padding search-result-card"; // Highlight matching snippet let snippet = ""; const matchingLine = item.textLines.find(line => line.toLowerCase().includes(query)); if (matchingLine) { snippet = matchingLine; } else if (item.textLines.length > 0) { snippet = item.textLines[0]; } // Highlight text snippet helper const queryIdx = snippet.toLowerCase().indexOf(query); if (queryIdx !== -1 && query.length > 0) { const start = snippet.substring(0, queryIdx); const match = snippet.substring(queryIdx, queryIdx + query.length); const end = snippet.substring(queryIdx + query.length); snippet = `${start}${match}${end}`; } card.innerHTML = `
Slide Thumb
${item.pptTitle} · 第 ${item.slideNumber} 頁
${item.title || '(無標題)'}
${snippet}
`; card.addEventListener("click", () => { jumpToPresentation(item.pptId, item.slideNumber); }); searchResults.appendChild(card); }); }); } // 7. Key Highlights Section Controller const highlightsData = [ { pptId: "ppt1", slideNum: 6, category: "network", title: "中央氣象署即時地震觀測網分佈", description: "展示臺灣陸地與海域鋪設的極其密集的觀測網。包含強震儀站點與鋪設於東部海域、南部海域的 OBS 海底電纜觀測系統,是提供即時測報之根本基礎。", keyPoints: ["島內密集陸地強震儀分佈", "東部與南部海纜觀測系統 (OBS)", "即時連續訊號在發震數秒內傳達中心"] }, { pptId: "ppt1", slideNum: 11, category: "network", title: "實時地震波形歸檔整合數據流", description: "展示 Earthworm 處理器如何透過共享記憶體 RING,將 Field Stations 的資料利用 tbuf2mseed 模組壓縮,並同步保存至本機 NAS,確保高可靠度與極低資料延遲。", keyPoints: ["共享記憶體 (Shared Memory) 機制", "支援跨網絡與跨節點資料交換", "Docker 容器化保證部署彈性"] }, { pptId: "ppt1", slideNum: 12, category: "network", title: "eBEAR 自動化強震即時警報軟體", description: "CWA 自主開發的 eBEAR (Earthworm Based Earthquake Alert Reporting) 系統,已封裝成 Docker Image 並提供在 Docker Hub 下載,具備模組化警報回報流程。", keyPoints: ["Docker Hub: cwadayi/earthworm_ubuntu22.04_eew:v1", "高整合性的地震波相拾取與規模演算", "支援自動化的警報派遣與日誌紀錄"] }, { pptId: "ppt2", slideNum: 2, category: "eew", title: "2026年5月 強震即時警報系統 (EEW) 效能", description: "彙整 2026 年 5 月份 EEW 的統計數據。最快解算時效達到驚人的 9.7 秒,PWS 細胞廣播發送 6 次,展示了防減災技術在即時解算上的優異成果。", keyPoints: ["5月份地震預警最速時效達 9.7 秒", "PWS 發布 6 次,電視台即時插播 6 次", "對外通報管道觸發穩定,無漏報異常"] }, { pptId: "ppt2", slideNum: 14, category: "eew", title: "應變盲區 (Blind Zone) 與 S 波幾何學", description: "地震發生後,P 波與 S 波以球形向外傳播。在系統花費時間解算並發送警報前,S 波已波及的範圍(半徑 = 解算時間 × S波速度)即為預警盲區,此區域內前置時間為零。", keyPoints: ["盲區半徑 = 解算時間 (s) × S波速度 (3.5km/s)", "極淺地震(如大埔或美濃)的震央周圍多位於盲區內", "加速測站密集度與算法能有效縮小盲區"] }, { pptId: "ppt2", slideNum: 15, category: "eew", title: "測站觸發率 (Trigger Rate) 半徑篩選", description: "以定位震央為中心、事件最遠觸發測站為半徑作圓,評估該圓形範圍內觀測網測站觸發率,排除共站造成的比率偏低誤差,是評估觀測網效能的關鍵指標。", keyPoints: ["速報震央為圓心,最遠觸發測站為半徑作圓", "剔除重複計算共站,反映真實觀測網覆蓋", "輔助評估密集強震時之儀器健康度"] }, { pptId: "ppt3", slideNum: 4, category: "history", title: "臺灣過去 10 年災害性地震損失統計", description: "展示臺灣過去 10 年內數次引發嚴重災情之強震(包括美濃、花蓮、池上、大埔地震等),凸顯了台灣極高地震風險性與建立次世代預警系統的迫切性。", keyPoints: ["美濃地震維冠大樓塌陷、花蓮地震統帥飯店受災", "高鐵與捷運在預警下成功採取減速避災措施", "災害地震平均每 30-40 年發生一次"] }, { pptId: "ppt3", slideNum: 44, category: "ai", title: "大型地震模型 LEM 與 SeisWav2Vec 2.0", description: "介紹將 Wav2Vec 2.0 語音自監督模型遷移至地震波形特徵提取之架構。由一維 CNN 編碼器與 12 層 Transformer 構成,從無標記波形中自主學習震波語意特徵。", keyPoints: ["自監督學習 (SSL) 無需人工標註海量資料", "Wav2Vec 離散向量量化與 Mask 遮罩學習", "微調後於波相辨識精度與噪訊過濾上顯著提升"] }, { pptId: "ppt3", slideNum: 50, category: "ai", title: "EEW Swarm 四位 Agent 角色與職責矩陣", description: "次世代地震系統中四個協同 Agents 的核心分工矩陣:包含主助理、科學前向轉發工程師、地震分析學助教與本地排程容器整合工程師,建構多代理人體系。", keyPoints: ["my_agent、seismo、seismo_agent、secondary_agent", "實現跨系統的多代理協調派遣", "個別 Agent 維持獨立的指令執行環境與日誌監控"] }, { pptId: "ppt3", slideNum: 54, category: "ai", title: "EEW 代理群多模組自動化閉環工作流", description: "當系統產出新事件的 `.rep` 報告後,自動觸發 seismo 解析資料、seismo_agent 繪製散佈圖、secondary_agent 協調排程、最後由 my_agent 生成簡報推播至 Telegram 的自動化流向圖。", keyPoints: ["事件驅動 (Event-driven) 觸發運行", "Telegram API 即時回報與 eew_fault_recovery 恢復功能", "全流程無人工介入,秒級產出簡報與日報"] }, { pptId: "ppt4", slideNum: 18, category: "eew", title: "強震即時警報物理原理 (P/S 波速差)", description: "圖解地震產生之 P 縱波 (速度快) 與 S 橫波 (破壞大、速度慢) 的傳播物理機制。利用兩者速度差,可在 S 波抵達前數秒解算位置並以電磁波速度搶先警報。", keyPoints: ["P波波速約 6 km/s,S波波速約 3.5 km/s", "電磁波傳輸警報以光速前進,遠快於地震波", "提供關鍵數秒至數十秒避難避險前置時間"] }, { pptId: "ppt4", slideNum: 21, category: "history", title: "2024/04/03 M7.2 花蓮大地震國家級警報實況", description: "回顧花蓮強震當日預警系統運行狀況,說明系統於極短時間內完成解算,並透過細胞廣播 (PWS)、電視插播與中小學防震系統即時推送警報的實際情境。", keyPoints: ["全台多個縣市獲得寶貴避險前置時間", "電視台實時彈出插播覆蓋全螢幕", "分析極震區盲區覆蓋以及大眾運輸系統防災減速反應"] } ]; function initHighlights() { const container = document.getElementById("highlights-container"); const filterButtons = document.querySelectorAll(".filter-btn"); if (!container) return; const renderHighlights = (catFilter) => { container.innerHTML = ""; const filtered = catFilter === "all" ? highlightsData : highlightsData.filter(h => h.category === catFilter); filtered.forEach(h => { const card = document.createElement("div"); card.className = "glass-panel"; card.style.display = "flex"; card.style.flexDirection = "column"; card.style.overflow = "hidden"; const slideImgPath = `slides/${h.pptId}/slide_${h.slideNum}.png`; // Category tag display const catLabels = { network: "觀測架構", eew: "預警效能", ai: "AI與代理人", history: "地震與案例" }; const catLabel = catLabels[h.category] || h.category; card.innerHTML = `
${h.title} ${h.pptId.toUpperCase()} · Slide ${h.slideNum} ${catLabel}

${h.title}

${h.description}

`; // Lightbox click handler card.querySelector(".highlight-img-click").addEventListener("click", () => { lightroomImgEl.src = slideImgPath; lightroomEl.style.display = "flex"; }); // Slide Jump click handler card.querySelector(".jump-to-slide-btn").addEventListener("click", () => { jumpToPresentation(h.pptId, h.slideNum); }); container.appendChild(card); }); }; // Filter Buttons binding filterButtons.forEach(btn => { btn.addEventListener("click", () => { filterButtons.forEach(b => { b.classList.remove("active-filter"); b.style.background = ""; b.style.borderColor = ""; }); btn.classList.add("active-filter"); btn.style.background = "var(--accent-cyan-glow)"; btn.style.borderColor = "var(--accent-cyan)"; const filterCat = btn.getAttribute("data-filter-cat"); renderHighlights(filterCat); }); }); // Initial render renderHighlights("all"); }