Spaces:
Running
Running
| /** | |
| * Chronos-2 Zero-Shot Demo - Frontend | |
| * Real-time input -> debounce -> fetch forecast -> chart update | |
| * Tab: Forecast | Compare | |
| */ | |
| (function () { | |
| const API_BASE = ''; | |
| const tsInput = document.getElementById('ts-input'); | |
| const sampleSelect = document.getElementById('sample-select'); | |
| const predStepsInput = document.getElementById('pred-steps'); | |
| const forecastValue = document.getElementById('forecast-value'); | |
| const forecastRange = document.getElementById('forecast-range'); | |
| const loadingEl = document.getElementById('loading'); | |
| const errorEl = document.getElementById('error'); | |
| const downloadBtn = document.getElementById('download-csv'); | |
| let chart = null; | |
| let lastResult = null; | |
| let debounceTimer = null; | |
| // --------------------------------------------------------------------------- | |
| // Tabs | |
| // --------------------------------------------------------------------------- | |
| function initTabs() { | |
| const tabBtns = document.querySelectorAll('.tab-btn'); | |
| const tabContents = document.querySelectorAll('.tab-content'); | |
| tabBtns.forEach((btn) => { | |
| btn.addEventListener('click', () => { | |
| const tabId = btn.getAttribute('data-tab'); | |
| tabBtns.forEach((b) => b.classList.remove('active')); | |
| tabContents.forEach((c) => { | |
| c.classList.toggle('active', c.id === 'tab-' + tabId); | |
| }); | |
| btn.classList.add('active'); | |
| }); | |
| }); | |
| } | |
| initTabs(); | |
| // --------------------------------------------------------------------------- | |
| // Parse | |
| // --------------------------------------------------------------------------- | |
| function parseValues(text) { | |
| if (!text || !text.trim()) return []; | |
| const parts = text.trim().split(/[\s,;\n]+/); | |
| const values = []; | |
| for (const p of parts) { | |
| const n = parseFloat(p); | |
| if (!isNaN(n)) values.push(n); | |
| } | |
| return values; | |
| } | |
| function debounce(fn, ms) { | |
| return function () { | |
| clearTimeout(debounceTimer); | |
| debounceTimer = setTimeout(fn, ms); | |
| }; | |
| } | |
| async function fetchForecast(values, predictionLength) { | |
| const res = await fetch(`${API_BASE}/api/forecast`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ values, prediction_length: predictionLength }), | |
| }); | |
| if (!res.ok) { | |
| const err = await res.json().catch(() => ({ detail: res.statusText })); | |
| const d = err.detail; | |
| const msg = Array.isArray(d) | |
| ? d.map((x) => x.msg || JSON.stringify(x)).join('; ') | |
| : typeof d === 'string' | |
| ? d | |
| : JSON.stringify(d || err); | |
| throw new Error(msg || `HTTP ${res.status}`); | |
| } | |
| return res.json(); | |
| } | |
| function setLoading(loading) { | |
| loadingEl.style.display = loading ? 'block' : 'none'; | |
| errorEl.style.display = 'none'; | |
| } | |
| function setError(msg) { | |
| errorEl.textContent = msg; | |
| errorEl.style.display = msg ? 'block' : 'none'; | |
| loadingEl.style.display = 'none'; | |
| } | |
| function updateForecastDisplay(result) { | |
| downloadBtn.disabled = !result || !result.forecast.length; | |
| if (!result || !result.forecast.length) { | |
| forecastValue.textContent = 'β'; | |
| forecastRange.textContent = ''; | |
| return; | |
| } | |
| const f = result.forecast[0]; | |
| forecastValue.textContent = f.median.toFixed(2); | |
| forecastRange.textContent = ` (${f.low.toFixed(1)} ~ ${f.high.toFixed(1)})`; | |
| if (result.forecast.length > 1) { | |
| forecastRange.textContent += ` Β· ${result.forecast.length} steps`; | |
| } | |
| } | |
| function updateChart(historical, forecast) { | |
| const histLabels = (historical || []).map((_, i) => String(i)); | |
| const histData = (historical || []).map((h) => h.value); | |
| const forecastLabels = (forecast || []).map((f) => String(f.index)); | |
| const forecastMedians = (forecast || []).map((f) => f.median); | |
| const allLabels = histLabels.concat(forecastLabels); | |
| const histFull = histData.concat(forecastMedians.map(() => null)); | |
| const fcastFull = histLabels.map(() => null).concat(forecastMedians); | |
| if (!chart) { | |
| chart = new Chart(document.getElementById('chart'), { | |
| type: 'line', | |
| data: { | |
| labels: allLabels, | |
| datasets: [ | |
| { | |
| label: 'Historical', | |
| data: histFull, | |
| borderColor: 'rgb(59, 130, 246)', | |
| fill: false, | |
| tension: 0.2, | |
| }, | |
| { | |
| label: 'Forecast (median)', | |
| data: fcastFull, | |
| borderColor: 'rgb(239, 68, 68)', | |
| borderDash: [5, 5], | |
| fill: false, | |
| tension: 0.2, | |
| }, | |
| ], | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: true, | |
| plugins: { legend: { position: 'top' } }, | |
| scales: { | |
| x: { title: { display: true, text: 'Index' } }, | |
| y: { title: { display: true, text: 'Value' } }, | |
| }, | |
| }, | |
| }); | |
| } else { | |
| chart.data.labels = allLabels; | |
| chart.data.datasets[0].data = histFull; | |
| chart.data.datasets[1].data = fcastFull; | |
| chart.update(); | |
| } | |
| } | |
| async function triggerForecast() { | |
| const values = parseValues(tsInput.value); | |
| const predictionLength = Math.max(1, parseInt(predStepsInput.value, 10) || 1); | |
| if (values.length === 0) { | |
| lastResult = null; | |
| updateForecastDisplay(null); | |
| updateChart([], []); | |
| setError(''); | |
| return; | |
| } | |
| setLoading(true); | |
| setError(''); | |
| try { | |
| const result = await fetchForecast(values, predictionLength); | |
| lastResult = result; | |
| updateForecastDisplay(result); | |
| updateChart(result.historical, result.forecast); | |
| } catch (err) { | |
| setError(err.message || 'μμΈ‘ μ€ μ€λ₯κ° λ°μνμ΅λλ€.'); | |
| lastResult = null; | |
| updateForecastDisplay(null); | |
| updateChart( | |
| values.map((v, i) => ({ index: i, value: v })), | |
| [] | |
| ); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| const debouncedForecast = debounce(triggerForecast, 500); | |
| async function loadSample(name) { | |
| try { | |
| const res = await fetch(`${API_BASE}/static/samples/${name}.json`); | |
| if (!res.ok) throw new Error('Sample not found'); | |
| const data = await res.json(); | |
| const values = data.values || data; | |
| tsInput.value = Array.isArray(values) ? values.join(', ') : String(values); | |
| debouncedForecast(); | |
| } catch (err) { | |
| setError('μνμ λΆλ¬μ¬ μ μμ΅λλ€.'); | |
| } | |
| } | |
| function downloadCsv() { | |
| if (!lastResult || !lastResult.forecast.length) return; | |
| const headers = ['index', 'median', 'low', 'high']; | |
| const rows = lastResult.forecast.map((f) => [f.index, f.median, f.low, f.high].join(',')); | |
| const csv = [headers.join(','), ...rows].join('\n'); | |
| const blob = new Blob([csv], { type: 'text/csv' }); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = 'chronos2_forecast.csv'; | |
| a.click(); | |
| URL.revokeObjectURL(a.href); | |
| } | |
| tsInput.addEventListener('input', debouncedForecast); | |
| predStepsInput.addEventListener('change', debouncedForecast); | |
| sampleSelect.addEventListener('change', function () { | |
| const v = this.value; | |
| if (v) { | |
| loadSample(v); | |
| this.value = ''; | |
| } | |
| }); | |
| downloadBtn.addEventListener('click', downloadCsv); | |
| updateForecastDisplay(null); | |
| updateChart([], []); | |
| // νμ΄μ§ λ‘λ μ GFK μν λ°μ΄ν°λ₯Ό κΈ°λ³Έκ°μΌλ‘ λ‘λ | |
| loadSample('gfk_sample'); | |
| })(); | |