3v324v23 factory-droid[bot] commited on
Commit
d9dc826
·
1 Parent(s): 19c8775

Add Excel Add-in v2.1.0 with static files

Browse files

- Added FastAPI StaticFiles support for serving Add-in
- Added /static directory with manifest.xml, HTML, JS, CSS
- Updated app/main.py with StaticFiles mounts and /manifest.xml endpoint
- Updated Dockerfile to include static/ directory
- CORS configured for HF Space URL and Office Add-ins
- Version bumped to 2.1.0

Excel Add-in features:
- Univariate Forecast
- Multi-Series Forecast
- Forecast with Covariates (NEW)
- Scenario Analysis (NEW)
- Multivariate Forecast (NEW)
- Anomaly Detection
- Backtest

Users can now load the Add-in directly from:
https://ttzzs-chronos2-excel-forecasting-api.hf.space/manifest.xml

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>

Dockerfile CHANGED
@@ -26,6 +26,9 @@ RUN pip install --no-cache-dir --upgrade pip && \
26
  # Copiar código de la aplicación
27
  COPY app/ ./app/
28
 
 
 
 
29
  # Crear usuario no-root
30
  RUN useradd -m -u 1000 user && \
31
  chown -R user:user /app
 
26
  # Copiar código de la aplicación
27
  COPY app/ ./app/
28
 
29
+ # Copiar archivos estáticos del Excel Add-in
30
+ COPY static/ ./static/
31
+
32
  # Crear usuario no-root
33
  RUN useradd -m -u 1000 user && \
34
  chown -R user:user /app
app/main.py CHANGED
@@ -5,6 +5,8 @@ import numpy as np
5
  import pandas as pd
6
  from fastapi import FastAPI, HTTPException
7
  from fastapi.middleware.cors import CORSMiddleware
 
 
8
  from pydantic import BaseModel, Field
9
 
10
  from chronos import Chronos2Pipeline
@@ -18,18 +20,24 @@ MODEL_ID = os.getenv("CHRONOS_MODEL_ID", "amazon/chronos-2")
18
  DEVICE_MAP = os.getenv("DEVICE_MAP", "cpu") # "cpu" o "cuda"
19
 
20
  app = FastAPI(
21
- title="Chronos-2 Universal Forecasting API",
22
  description=(
23
- "Servidor local (Docker) para pronósticos con Chronos-2: univariante, "
24
- "multivariante, covariables, escenarios, anomalías y backtesting."
 
25
  ),
26
- version="1.0.0",
27
  )
28
 
29
  # Configurar CORS para Excel Add-in
30
  app.add_middleware(
31
  CORSMiddleware,
32
- allow_origins=["https://localhost:3001", "https://localhost:3000"],
 
 
 
 
 
33
  allow_credentials=True,
34
  allow_methods=["*"],
35
  allow_headers=["*"],
@@ -38,6 +46,72 @@ app.add_middleware(
38
  # Carga única del modelo al iniciar el proceso
39
  pipeline = Chronos2Pipeline.from_pretrained(MODEL_ID, device_map=DEVICE_MAP)
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
  # =========================
43
  # Modelos Pydantic comunes
 
5
  import pandas as pd
6
  from fastapi import FastAPI, HTTPException
7
  from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.staticfiles import StaticFiles
9
+ from fastapi.responses import FileResponse
10
  from pydantic import BaseModel, Field
11
 
12
  from chronos import Chronos2Pipeline
 
20
  DEVICE_MAP = os.getenv("DEVICE_MAP", "cpu") # "cpu" o "cuda"
21
 
22
  app = FastAPI(
23
+ title="Chronos-2 Universal Forecasting API + Excel Add-in",
24
  description=(
25
+ "Servidor para pronósticos con Chronos-2: univariante, "
26
+ "multivariante, covariables, escenarios, anomalías y backtesting. "
27
+ "Incluye Excel Add-in v2.1.0 con archivos estáticos."
28
  ),
29
+ version="2.1.0",
30
  )
31
 
32
  # Configurar CORS para Excel Add-in
33
  app.add_middleware(
34
  CORSMiddleware,
35
+ allow_origins=[
36
+ "https://localhost:3001",
37
+ "https://localhost:3000",
38
+ "https://ttzzs-chronos2-excel-forecasting-api.hf.space",
39
+ "*" # Permitir todos los orígenes para Office Add-ins
40
+ ],
41
  allow_credentials=True,
42
  allow_methods=["*"],
43
  allow_headers=["*"],
 
46
  # Carga única del modelo al iniciar el proceso
47
  pipeline = Chronos2Pipeline.from_pretrained(MODEL_ID, device_map=DEVICE_MAP)
48
 
49
+ # =========================
50
+ # Archivos estáticos para Excel Add-in
51
+ # =========================
52
+
53
+ # Montar directorios estáticos si existen
54
+ if os.path.exists("static"):
55
+ app.mount("/assets", StaticFiles(directory="static/assets"), name="assets")
56
+ app.mount("/taskpane", StaticFiles(directory="static/taskpane"), name="taskpane")
57
+ app.mount("/commands", StaticFiles(directory="static/commands"), name="commands")
58
+
59
+ # Endpoint para manifest.xml
60
+ @app.get("/manifest.xml", response_class=FileResponse)
61
+ async def get_manifest():
62
+ """Devuelve el manifest.xml del Excel Add-in"""
63
+ return FileResponse("static/manifest.xml", media_type="application/xml")
64
+
65
+ @app.get("/", tags=["Info"])
66
+ async def root_with_addon():
67
+ """Información del API + Add-in"""
68
+ return {
69
+ "name": "Chronos-2 Forecasting API",
70
+ "version": "2.1.0",
71
+ "model": MODEL_ID,
72
+ "endpoints": {
73
+ "api": [
74
+ "/health",
75
+ "/forecast_univariate",
76
+ "/forecast_multi_id",
77
+ "/forecast_with_covariates",
78
+ "/forecast_multivariate",
79
+ "/forecast_scenarios",
80
+ "/detect_anomalies",
81
+ "/backtest_simple"
82
+ ],
83
+ "add_in": [
84
+ "/manifest.xml",
85
+ "/taskpane/taskpane.html",
86
+ "/assets/icon-*.png"
87
+ ]
88
+ },
89
+ "docs": "/docs",
90
+ "excel_add_in": {
91
+ "manifest_url": "https://ttzzs-chronos2-excel-forecasting-api.hf.space/manifest.xml",
92
+ "version": "2.1.0",
93
+ "features": [
94
+ "Univariate Forecast",
95
+ "Multi-Series Forecast",
96
+ "Forecast with Covariates",
97
+ "Scenario Analysis",
98
+ "Multivariate Forecast",
99
+ "Anomaly Detection",
100
+ "Backtest"
101
+ ]
102
+ }
103
+ }
104
+ else:
105
+ @app.get("/", tags=["Info"])
106
+ async def root_api_only():
107
+ """Información del API (sin Add-in)"""
108
+ return {
109
+ "name": "Chronos-2 Forecasting API",
110
+ "version": "2.1.0",
111
+ "model": MODEL_ID,
112
+ "docs": "/docs"
113
+ }
114
+
115
 
116
  # =========================
117
  # Modelos Pydantic comunes
static/assets/icon-16.png ADDED
static/assets/icon-32.png ADDED
static/assets/icon-64.png ADDED
static/assets/icon-80.png ADDED
static/commands/commands.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
6
+ </head>
7
+ <body>
8
+ <!-- Ribbon commands handler -->
9
+ <script>
10
+ Office.onReady(() => {
11
+ console.log('Chronos2 commands loaded');
12
+ });
13
+ </script>
14
+ </body>
15
+ </html>
static/manifest.xml ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+ <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
3
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
+ xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
5
+ xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides"
6
+ xsi:type="TaskPaneApp">
7
+
8
+ <!-- Configuración básica -->
9
+ <Id>c8a6e7f2-3d4b-4f5a-8b9c-1e2d3f4a5b6c</Id>
10
+ <Version>2.1.0</Version>
11
+ <ProviderName>Chronos2 Forecasting</ProviderName>
12
+ <DefaultLocale>en-US</DefaultLocale>
13
+
14
+ <!-- Información de visualización -->
15
+ <DisplayName DefaultValue="Chronos2 Forecasting v2.1" />
16
+ <Description DefaultValue="AI-powered time series forecasting with Chronos-2. Now with Covariates, Scenarios, and Multivariate support!"/>
17
+ <IconUrl DefaultValue="https://ttzzs-chronos2-excel-forecasting-api.hf.space/assets/icon-32.png"/>
18
+ <HighResolutionIconUrl DefaultValue="https://ttzzs-chronos2-excel-forecasting-api.hf.space/assets/icon-64.png"/>
19
+ <SupportUrl DefaultValue="https://ttzzs-chronos2-excel-forecasting-api.hf.space"/>
20
+
21
+ <!-- Hosts soportados -->
22
+ <Hosts>
23
+ <Host Name="Workbook" />
24
+ </Hosts>
25
+
26
+ <DefaultSettings>
27
+ <SourceLocation DefaultValue="https://ttzzs-chronos2-excel-forecasting-api.hf.space/taskpane/taskpane.html"/>
28
+ </DefaultSettings>
29
+
30
+ <!-- Permisos -->
31
+ <Permissions>ReadWriteDocument</Permissions>
32
+
33
+ <!-- Versión específica para Excel -->
34
+ <VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
35
+ <Hosts>
36
+ <Host xsi:type="Workbook">
37
+
38
+ <!-- Ribbon personalizado -->
39
+ <DesktopFormFactor>
40
+ <GetStarted>
41
+ <Title resid="GetStarted.Title"/>
42
+ <Description resid="GetStarted.Description"/>
43
+ <LearnMoreUrl resid="GetStarted.LearnMoreUrl"/>
44
+ </GetStarted>
45
+
46
+ <FunctionFile resid="Commands.Url" />
47
+
48
+ <ExtensionPoint xsi:type="PrimaryCommandSurface">
49
+ <OfficeTab id="TabHome">
50
+ <Group id="CommandsGroup">
51
+ <Label resid="CommandsGroup.Label" />
52
+ <Icon>
53
+ <bt:Image size="16" resid="Icon.16x16" />
54
+ <bt:Image size="32" resid="Icon.32x32" />
55
+ <bt:Image size="80" resid="Icon.80x80" />
56
+ </Icon>
57
+
58
+ <!-- Botón principal -->
59
+ <Control xsi:type="Button" id="TaskpaneButton">
60
+ <Label resid="TaskpaneButton.Label" />
61
+ <Supertip>
62
+ <Title resid="TaskpaneButton.Label" />
63
+ <Description resid="TaskpaneButton.Tooltip" />
64
+ </Supertip>
65
+ <Icon>
66
+ <bt:Image size="16" resid="Icon.16x16" />
67
+ <bt:Image size="32" resid="Icon.32x32" />
68
+ <bt:Image size="80" resid="Icon.80x80" />
69
+ </Icon>
70
+ <Action xsi:type="ShowTaskpane">
71
+ <TaskpaneId>ButtonId1</TaskpaneId>
72
+ <SourceLocation resid="Taskpane.Url" />
73
+ </Action>
74
+ </Control>
75
+ </Group>
76
+ </OfficeTab>
77
+ </ExtensionPoint>
78
+ </DesktopFormFactor>
79
+ </Host>
80
+ </Hosts>
81
+
82
+ <!-- Recursos -->
83
+ <Resources>
84
+ <bt:Images>
85
+ <bt:Image id="Icon.16x16" DefaultValue="https://ttzzs-chronos2-excel-forecasting-api.hf.space/assets/icon-16.png"/>
86
+ <bt:Image id="Icon.32x32" DefaultValue="https://ttzzs-chronos2-excel-forecasting-api.hf.space/assets/icon-32.png"/>
87
+ <bt:Image id="Icon.80x80" DefaultValue="https://ttzzs-chronos2-excel-forecasting-api.hf.space/assets/icon-80.png"/>
88
+ </bt:Images>
89
+ <bt:Urls>
90
+ <bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://ttzzs-chronos2-excel-forecasting-api.hf.space"/>
91
+ <bt:Url id="Commands.Url" DefaultValue="https://ttzzs-chronos2-excel-forecasting-api.hf.space/commands/commands.html"/>
92
+ <bt:Url id="Taskpane.Url" DefaultValue="https://ttzzs-chronos2-excel-forecasting-api.hf.space/taskpane/taskpane.html"/>
93
+ </bt:Urls>
94
+ <bt:ShortStrings>
95
+ <bt:String id="GetStarted.Title" DefaultValue="Get started with Chronos2!" />
96
+ <bt:String id="CommandsGroup.Label" DefaultValue="Chronos2" />
97
+ <bt:String id="TaskpaneButton.Label" DefaultValue="Forecasting" />
98
+ </bt:ShortStrings>
99
+ <bt:LongStrings>
100
+ <bt:String id="GetStarted.Description" DefaultValue="AI-powered forecasting loaded successfully. Click the Forecasting button to start." />
101
+ <bt:String id="TaskpaneButton.Tooltip" DefaultValue="Open Chronos2 Forecasting panel with v2.1.0 features: Covariates, Scenarios, Multivariate" />
102
+ </bt:LongStrings>
103
+ </Resources>
104
+ </VersionOverrides>
105
+ </OfficeApp>
static/taskpane/taskpane.css ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ====================================================================
2
+ CHRONOS2 FORECASTING ADD-IN - STYLES
3
+ ==================================================================== */
4
+
5
+ * {
6
+ margin: 0;
7
+ padding: 0;
8
+ box-sizing: border-box;
9
+ }
10
+
11
+ body {
12
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
13
+ font-size: 14px;
14
+ line-height: 1.5;
15
+ color: #333;
16
+ background: #f5f7fa;
17
+ padding: 0;
18
+ margin: 0;
19
+ }
20
+
21
+ .container {
22
+ max-width: 100%;
23
+ padding: 16px;
24
+ }
25
+
26
+ /* Header */
27
+ .header {
28
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
29
+ color: white;
30
+ padding: 20px;
31
+ margin: -16px -16px 20px -16px;
32
+ text-align: center;
33
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
34
+ }
35
+
36
+ .header h1 {
37
+ font-size: 24px;
38
+ margin-bottom: 4px;
39
+ }
40
+
41
+ .subtitle {
42
+ font-size: 12px;
43
+ opacity: 0.9;
44
+ }
45
+
46
+ /* Status Card */
47
+ .status-card {
48
+ background: white;
49
+ border-radius: 8px;
50
+ padding: 12px;
51
+ margin-bottom: 16px;
52
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
53
+ }
54
+
55
+ .status-indicator {
56
+ display: flex;
57
+ align-items: center;
58
+ font-size: 13px;
59
+ }
60
+
61
+ .status-indicator .dot {
62
+ width: 10px;
63
+ height: 10px;
64
+ border-radius: 50%;
65
+ margin-right: 8px;
66
+ animation: pulse 2s infinite;
67
+ }
68
+
69
+ .status-indicator.online .dot {
70
+ background: #10b981;
71
+ }
72
+
73
+ .status-indicator.offline .dot {
74
+ background: #ef4444;
75
+ animation: none;
76
+ }
77
+
78
+ @keyframes pulse {
79
+ 0%, 100% { opacity: 1; }
80
+ 50% { opacity: 0.5; }
81
+ }
82
+
83
+ /* Tabs */
84
+ .tabs {
85
+ display: flex;
86
+ gap: 4px;
87
+ margin-bottom: 16px;
88
+ background: white;
89
+ padding: 4px;
90
+ border-radius: 8px;
91
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
92
+ }
93
+
94
+ .tab {
95
+ flex: 1;
96
+ background: transparent;
97
+ border: none;
98
+ padding: 8px 12px;
99
+ font-size: 12px;
100
+ font-weight: 500;
101
+ color: #666;
102
+ border-radius: 6px;
103
+ cursor: pointer;
104
+ transition: all 0.2s;
105
+ }
106
+
107
+ .tab:hover {
108
+ background: #f3f4f6;
109
+ }
110
+
111
+ .tab.active {
112
+ background: #667eea;
113
+ color: white;
114
+ }
115
+
116
+ /* Tab Content */
117
+ .tab-content {
118
+ display: none;
119
+ }
120
+
121
+ .tab-content.active {
122
+ display: block;
123
+ }
124
+
125
+ /* Cards */
126
+ .card {
127
+ background: white;
128
+ border-radius: 8px;
129
+ padding: 16px;
130
+ margin-bottom: 16px;
131
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
132
+ }
133
+
134
+ .card h2 {
135
+ font-size: 16px;
136
+ margin-bottom: 8px;
137
+ color: #1f2937;
138
+ }
139
+
140
+ .card p {
141
+ font-size: 13px;
142
+ color: #6b7280;
143
+ margin-bottom: 12px;
144
+ }
145
+
146
+ /* Form Groups */
147
+ .form-group {
148
+ margin-bottom: 12px;
149
+ }
150
+
151
+ .form-group label {
152
+ display: block;
153
+ font-size: 12px;
154
+ font-weight: 500;
155
+ margin-bottom: 4px;
156
+ color: #374151;
157
+ }
158
+
159
+ .form-group input,
160
+ .form-group select {
161
+ width: 100%;
162
+ padding: 8px 12px;
163
+ border: 1px solid #d1d5db;
164
+ border-radius: 6px;
165
+ font-size: 13px;
166
+ transition: border-color 0.2s;
167
+ }
168
+
169
+ .form-group input:focus,
170
+ .form-group select:focus {
171
+ outline: none;
172
+ border-color: #667eea;
173
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
174
+ }
175
+
176
+ /* Buttons */
177
+ .btn {
178
+ width: 100%;
179
+ padding: 10px 16px;
180
+ border: none;
181
+ border-radius: 6px;
182
+ font-size: 13px;
183
+ font-weight: 600;
184
+ cursor: pointer;
185
+ transition: all 0.2s;
186
+ margin-top: 8px;
187
+ }
188
+
189
+ .btn-primary {
190
+ background: #667eea;
191
+ color: white;
192
+ }
193
+
194
+ .btn-primary:hover {
195
+ background: #5568d3;
196
+ transform: translateY(-1px);
197
+ box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
198
+ }
199
+
200
+ .btn-secondary {
201
+ background: #10b981;
202
+ color: white;
203
+ }
204
+
205
+ .btn-secondary:hover {
206
+ background: #059669;
207
+ transform: translateY(-1px);
208
+ box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3);
209
+ }
210
+
211
+ .btn:active {
212
+ transform: translateY(0);
213
+ }
214
+
215
+ /* Info Box */
216
+ .info-box {
217
+ background: #eff6ff;
218
+ border-left: 3px solid #3b82f6;
219
+ padding: 12px;
220
+ border-radius: 4px;
221
+ font-size: 12px;
222
+ margin-bottom: 12px;
223
+ color: #1e40af;
224
+ }
225
+
226
+ /* Results Card */
227
+ .results-card {
228
+ background: white;
229
+ border-radius: 8px;
230
+ padding: 16px;
231
+ margin-top: 16px;
232
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
233
+ }
234
+
235
+ .results-card h3 {
236
+ font-size: 14px;
237
+ margin-bottom: 12px;
238
+ color: #1f2937;
239
+ }
240
+
241
+ .results-log {
242
+ max-height: 200px;
243
+ overflow-y: auto;
244
+ font-family: 'Monaco', 'Courier New', monospace;
245
+ font-size: 11px;
246
+ background: #f9fafb;
247
+ padding: 8px;
248
+ border-radius: 4px;
249
+ }
250
+
251
+ .log-entry {
252
+ padding: 4px 0;
253
+ border-bottom: 1px solid #e5e7eb;
254
+ }
255
+
256
+ .log-entry:last-child {
257
+ border-bottom: none;
258
+ }
259
+
260
+ .log-entry .timestamp {
261
+ color: #9ca3af;
262
+ margin-right: 8px;
263
+ }
264
+
265
+ .log-success {
266
+ color: #059669;
267
+ }
268
+
269
+ .log-error {
270
+ color: #dc2626;
271
+ }
272
+
273
+ .log-info {
274
+ color: #3b82f6;
275
+ }
276
+
277
+ /* Footer */
278
+ .footer {
279
+ text-align: center;
280
+ padding: 16px;
281
+ margin-top: 20px;
282
+ font-size: 11px;
283
+ color: #9ca3af;
284
+ }
285
+
286
+ .footer .version {
287
+ margin-top: 4px;
288
+ color: #d1d5db;
289
+ }
290
+
291
+ /* Scrollbar */
292
+ .results-log::-webkit-scrollbar {
293
+ width: 6px;
294
+ }
295
+
296
+ .results-log::-webkit-scrollbar-track {
297
+ background: #f1f1f1;
298
+ border-radius: 3px;
299
+ }
300
+
301
+ .results-log::-webkit-scrollbar-thumb {
302
+ background: #888;
303
+ border-radius: 3px;
304
+ }
305
+
306
+ .results-log::-webkit-scrollbar-thumb:hover {
307
+ background: #555;
308
+ }
static/taskpane/taskpane.html ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>Chronos2 Forecasting</title>
8
+
9
+ <!-- Office JavaScript API -->
10
+ <script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
11
+
12
+ <!-- Estilos -->
13
+ <link rel="stylesheet" href="taskpane.css" />
14
+ </head>
15
+
16
+ <body>
17
+ <div class="container">
18
+ <!-- Header -->
19
+ <header class="header">
20
+ <h1>🔮 Chronos2 Forecasting</h1>
21
+ <p class="subtitle">AI-Powered Time Series Forecasting</p>
22
+ </header>
23
+
24
+ <!-- Status del servidor -->
25
+ <div class="status-card">
26
+ <div class="status-indicator" id="serverStatus">
27
+ <span class="dot"></span>
28
+ <span id="statusText">Checking server...</span>
29
+ </div>
30
+ </div>
31
+
32
+ <!-- Tabs -->
33
+ <div class="tabs">
34
+ <button class="tab active" onclick="showTab('basic')">Basic</button>
35
+ <button class="tab" onclick="showTab('multi')">Multi-Series</button>
36
+ <button class="tab" onclick="showTab('covariates')">Covariates</button>
37
+ <button class="tab" onclick="showTab('scenarios')">Scenarios</button>
38
+ </div>
39
+
40
+ <!-- Tab 1: Funciones Básicas -->
41
+ <div id="tab-basic" class="tab-content active">
42
+ <div class="card">
43
+ <h2>📊 Univariate Forecast</h2>
44
+ <p>Select a range with your time series data</p>
45
+
46
+ <div class="form-group">
47
+ <label>Prediction Length:</label>
48
+ <input type="number" id="predictionLength" value="7" min="1" max="365" />
49
+ </div>
50
+
51
+ <div class="form-group">
52
+ <label>Frequency:</label>
53
+ <select id="frequency">
54
+ <option value="D">Daily</option>
55
+ <option value="W">Weekly</option>
56
+ <option value="M">Monthly</option>
57
+ <option value="H">Hourly</option>
58
+ <option value="Q">Quarterly</option>
59
+ </select>
60
+ </div>
61
+
62
+ <button class="btn btn-primary" onclick="forecastUnivariate()">
63
+ 🚀 Generate Forecast
64
+ </button>
65
+ </div>
66
+
67
+ <div class="card">
68
+ <h2>🔍 Anomaly Detection</h2>
69
+ <p>Detect outliers in your data</p>
70
+
71
+ <div class="form-group">
72
+ <label>Context Length:</label>
73
+ <input type="number" id="contextLength" value="20" min="5" />
74
+ </div>
75
+
76
+ <div class="form-group">
77
+ <label>Recent Points:</label>
78
+ <input type="number" id="recentPoints" value="7" min="1" />
79
+ </div>
80
+
81
+ <button class="btn btn-secondary" onclick="detectAnomalies()">
82
+ 🔍 Detect Anomalies
83
+ </button>
84
+ </div>
85
+
86
+ <div class="card">
87
+ <h2>📈 Backtest</h2>
88
+ <p>Evaluate model accuracy</p>
89
+
90
+ <div class="form-group">
91
+ <label>Test Length:</label>
92
+ <input type="number" id="testLength" value="7" min="1" />
93
+ </div>
94
+
95
+ <button class="btn btn-secondary" onclick="runBacktest()">
96
+ 📈 Run Backtest
97
+ </button>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- Tab 2: Multi-Series -->
102
+ <div id="tab-multi" class="tab-content">
103
+ <div class="card">
104
+ <h2>📊 Multi-Series Forecast</h2>
105
+ <p>Select data with multiple series (ID column + values)</p>
106
+
107
+ <div class="info-box">
108
+ <strong>Expected format:</strong><br>
109
+ Column A: Series ID<br>
110
+ Column B: Date<br>
111
+ Column C: Value
112
+ </div>
113
+
114
+ <div class="form-group">
115
+ <label>Prediction Length:</label>
116
+ <input type="number" id="multiPredLength" value="7" min="1" />
117
+ </div>
118
+
119
+ <button class="btn btn-primary" onclick="forecastMultiSeries()">
120
+ 🚀 Forecast All Series
121
+ </button>
122
+ </div>
123
+ </div>
124
+
125
+ <!-- Tab 3: Covariates -->
126
+ <div id="tab-covariates" class="tab-content">
127
+ <div class="card">
128
+ <h2>🎯 Forecast with Covariates</h2>
129
+ <p>Include explanatory variables (price, promotions, etc.)</p>
130
+
131
+ <div class="info-box">
132
+ <strong>Expected format:</strong><br>
133
+ Col A: Date<br>
134
+ Col B: Target variable<br>
135
+ Col C+: Covariates (price, promo, etc.)<br>
136
+ <br>
137
+ Future rows: Only covariates filled
138
+ </div>
139
+
140
+ <div class="form-group">
141
+ <label>Prediction Length:</label>
142
+ <input type="number" id="covPredLength" value="7" min="1" />
143
+ </div>
144
+
145
+ <div class="form-group">
146
+ <label>Covariate Columns:</label>
147
+ <input type="text" id="covariateNames" value="price,promotion,temperature"
148
+ placeholder="comma-separated names" />
149
+ </div>
150
+
151
+ <button class="btn btn-primary" onclick="forecastWithCovariates()">
152
+ 🎯 Generate Forecast
153
+ </button>
154
+ </div>
155
+
156
+ <div class="card">
157
+ <h2>🎲 What-If Scenarios</h2>
158
+ <p>Compare multiple scenarios</p>
159
+
160
+ <div class="form-group">
161
+ <label>Number of Scenarios:</label>
162
+ <input type="number" id="numScenarios" value="3" min="1" max="10" />
163
+ </div>
164
+
165
+ <button class="btn btn-secondary" onclick="generateScenarios()">
166
+ 🎲 Generate Scenarios
167
+ </button>
168
+ </div>
169
+ </div>
170
+
171
+ <!-- Tab 4: Scenarios -->
172
+ <div id="tab-scenarios" class="tab-content">
173
+ <div class="card">
174
+ <h2>🔮 Multivariate Forecast</h2>
175
+ <p>Multiple target variables simultaneously</p>
176
+
177
+ <div class="info-box">
178
+ <strong>Expected format:</strong><br>
179
+ Col A: Date<br>
180
+ Col B+: Multiple targets (sales, returns, stock, etc.)
181
+ </div>
182
+
183
+ <div class="form-group">
184
+ <label>Prediction Length:</label>
185
+ <input type="number" id="multivarPredLength" value="7" min="1" />
186
+ </div>
187
+
188
+ <div class="form-group">
189
+ <label>Target Columns:</label>
190
+ <input type="text" id="targetColumns" value="sales,returns,stock"
191
+ placeholder="comma-separated column names" />
192
+ </div>
193
+
194
+ <button class="btn btn-primary" onclick="forecastMultivariate()">
195
+ 🔮 Generate Forecast
196
+ </button>
197
+ </div>
198
+ </div>
199
+
200
+ <!-- Log de resultados -->
201
+ <div class="results-card">
202
+ <h3>📋 Results</h3>
203
+ <div id="results" class="results-log">
204
+ Ready to forecast...
205
+ </div>
206
+ </div>
207
+
208
+ <!-- Footer -->
209
+ <footer class="footer">
210
+ <p>Powered by Amazon Chronos-2 🤖</p>
211
+ <p class="version">v2.1.0 - Full Featured</p>
212
+ </footer>
213
+ </div>
214
+
215
+ <!-- Scripts -->
216
+ <script src="taskpane.js"></script>
217
+ </body>
218
+ </html>
static/taskpane/taskpane.js ADDED
@@ -0,0 +1,956 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* global Office, Excel, console */
2
+
3
+ // ====================================================================
4
+ // CHRONOS2 FORECASTING ADD-IN
5
+ // Office.js Task Pane Implementation
6
+ // ====================================================================
7
+
8
+ // URL del API en HuggingFace Spaces
9
+ const API_BASE_URL = 'https://ttzzs-chronos2-excel-forecasting-api.hf.space';
10
+
11
+ // Para desarrollo local, descomenta la siguiente línea:
12
+ // const API_BASE_URL = 'https://localhost:8000';
13
+
14
+ // Inicializar cuando Office esté listo
15
+ Office.onReady((info) => {
16
+ if (info.host === Office.HostType.Excel) {
17
+ console.log('Chronos2 Add-in loaded successfully');
18
+ checkServerStatus();
19
+
20
+ // Auto-check cada 30 segundos
21
+ setInterval(checkServerStatus, 30000);
22
+ }
23
+ });
24
+
25
+ // ====================================================================
26
+ // UTILIDADES
27
+ // ====================================================================
28
+
29
+ function log(message, type = 'info') {
30
+ const resultsDiv = document.getElementById('results');
31
+ const timestamp = new Date().toLocaleTimeString();
32
+ const icon = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️';
33
+
34
+ const entry = document.createElement('div');
35
+ entry.className = `log-entry log-${type}`;
36
+ entry.innerHTML = `<span class="timestamp">${timestamp}</span> ${icon} ${message}`;
37
+
38
+ resultsDiv.insertBefore(entry, resultsDiv.firstChild);
39
+
40
+ // Limitar a 20 entries
41
+ while (resultsDiv.children.length > 20) {
42
+ resultsDiv.removeChild(resultsDiv.lastChild);
43
+ }
44
+ }
45
+
46
+ async function checkServerStatus() {
47
+ try {
48
+ const response = await fetch(`${API_BASE_URL}/health`, {
49
+ method: 'GET',
50
+ headers: { 'Content-Type': 'application/json' }
51
+ });
52
+
53
+ const data = await response.json();
54
+
55
+ if (response.ok) {
56
+ updateServerStatus(true, `Connected - ${data.model_id}`);
57
+ } else {
58
+ updateServerStatus(false, 'Server error');
59
+ }
60
+ } catch (error) {
61
+ updateServerStatus(false, 'Server offline');
62
+ }
63
+ }
64
+
65
+ function updateServerStatus(isOnline, message) {
66
+ const statusEl = document.getElementById('serverStatus');
67
+ const textEl = document.getElementById('statusText');
68
+
69
+ statusEl.className = `status-indicator ${isOnline ? 'online' : 'offline'}`;
70
+ textEl.textContent = message;
71
+ }
72
+
73
+ function showTab(tabName) {
74
+ // Ocultar todos los tabs
75
+ const tabs = document.querySelectorAll('.tab-content');
76
+ tabs.forEach(tab => tab.classList.remove('active'));
77
+
78
+ const buttons = document.querySelectorAll('.tab');
79
+ buttons.forEach(btn => btn.classList.remove('active'));
80
+
81
+ // Mostrar el tab seleccionado
82
+ document.getElementById(`tab-${tabName}`).classList.add('active');
83
+ event.target.classList.add('active');
84
+ }
85
+
86
+ // ====================================================================
87
+ // FUNCIONES DE EXCEL (Office.js)
88
+ // ====================================================================
89
+
90
+ async function getSelectedRange() {
91
+ return Excel.run(async (context) => {
92
+ const range = context.workbook.getSelectedRange();
93
+ range.load('values, address');
94
+ await context.sync();
95
+
96
+ return {
97
+ values: range.values,
98
+ address: range.address
99
+ };
100
+ });
101
+ }
102
+
103
+ async function writeToRange(data, startCell) {
104
+ return Excel.run(async (context) => {
105
+ try {
106
+ console.log(`[writeToRange] Writing ${data?.length || 0} rows to ${startCell}`);
107
+ console.log('[writeToRange] Data:', JSON.stringify(data).substring(0, 200));
108
+
109
+ if (!data || data.length === 0) {
110
+ throw new Error('No data to write');
111
+ }
112
+
113
+ if (!data[0] || data[0].length === 0) {
114
+ throw new Error('Invalid data structure: empty first row');
115
+ }
116
+
117
+ const sheet = context.workbook.worksheets.getActiveWorksheet();
118
+ const numRows = data.length;
119
+ const numCols = data[0].length;
120
+
121
+ console.log(`[writeToRange] Creating range: ${numRows} rows x ${numCols} cols from ${startCell}`);
122
+
123
+ const range = sheet.getRange(startCell).getResizedRange(numRows - 1, numCols - 1);
124
+
125
+ range.values = data;
126
+ range.format.autofitColumns();
127
+
128
+ await context.sync();
129
+
130
+ console.log('[writeToRange] ✅ Data written successfully');
131
+ } catch (error) {
132
+ console.error('[writeToRange] ❌ Error:', error);
133
+ console.error('[writeToRange] Stack:', error.stack);
134
+ throw error;
135
+ }
136
+ });
137
+ }
138
+
139
+ async function writeForecastResults(timestamps, median, q10, q90, startRow) {
140
+ return Excel.run(async (context) => {
141
+ try {
142
+ console.log('[writeForecastResults] Starting...');
143
+ console.log(`[writeForecastResults] timestamps: ${timestamps?.length || 0} items`);
144
+ console.log(`[writeForecastResults] median: ${median?.length || 0} items`);
145
+ console.log(`[writeForecastResults] q10: ${q10?.length || 0} items`);
146
+ console.log(`[writeForecastResults] q90: ${q90?.length || 0} items`);
147
+ console.log(`[writeForecastResults] startRow: ${startRow}`);
148
+
149
+ // VALIDACIÓN
150
+ if (!timestamps || !median) {
151
+ throw new Error('Invalid data: timestamps or median is undefined');
152
+ }
153
+
154
+ if (timestamps.length === 0) {
155
+ throw new Error('No forecast data received (empty timestamps)');
156
+ }
157
+
158
+ if (timestamps.length !== median.length) {
159
+ throw new Error(`Data mismatch: ${timestamps.length} timestamps vs ${median.length} median values`);
160
+ }
161
+
162
+ const sheet = context.workbook.worksheets.getActiveWorksheet();
163
+
164
+ // Preparar datos
165
+ const data = [];
166
+ data.push(['Timestamp', 'Median', 'Q10', 'Q90']); // Headers
167
+
168
+ for (let i = 0; i < timestamps.length; i++) {
169
+ data.push([
170
+ timestamps[i],
171
+ median[i],
172
+ q10 ? q10[i] : '',
173
+ q90 ? q90[i] : ''
174
+ ]);
175
+ }
176
+
177
+ console.log(`[writeForecastResults] Prepared ${data.length} rows (including header)`);
178
+
179
+ // Escribir en columnas D-G a partir de la fila especificada
180
+ const startCell = `D${startRow}`;
181
+ console.log(`[writeForecastResults] Writing to ${startCell}`);
182
+
183
+ await writeToRange(data, startCell);
184
+
185
+ // Aplicar formato
186
+ const headerRange = sheet.getRange(`D${startRow}:G${startRow}`);
187
+ headerRange.format.font.bold = true;
188
+ headerRange.format.fill.color = '#4472C4';
189
+ headerRange.format.font.color = 'white';
190
+
191
+ await context.sync();
192
+
193
+ console.log('[writeForecastResults] ✅ Forecast results written successfully');
194
+ } catch (error) {
195
+ console.error('[writeForecastResults] ❌ Error:', error);
196
+ console.error('[writeForecastResults] Stack:', error.stack);
197
+ throw error;
198
+ }
199
+ });
200
+ }
201
+
202
+ // ====================================================================
203
+ // FUNCIÓN 1: PRONÓSTICO UNIVARIANTE
204
+ // ====================================================================
205
+
206
+ async function forecastUnivariate() {
207
+ log('Starting univariate forecast...');
208
+
209
+ try {
210
+ // Leer rango seleccionado
211
+ const selection = await getSelectedRange();
212
+ const values = selection.values.flat().filter(v => v !== '' && !isNaN(v));
213
+
214
+ if (values.length < 3) {
215
+ log('Error: Select at least 3 data points', 'error');
216
+ return;
217
+ }
218
+
219
+ log(`Selected ${values.length} data points from ${selection.address}`);
220
+
221
+ // Obtener parámetros
222
+ const predictionLength = parseInt(document.getElementById('predictionLength').value);
223
+ const frequency = document.getElementById('frequency').value;
224
+
225
+ // Construir request
226
+ const requestBody = {
227
+ prediction_length: predictionLength,
228
+ series: { values: values },
229
+ start_timestamp: new Date().toISOString().split('T')[0],
230
+ freq: frequency,
231
+ quantile_levels: [0.1, 0.5, 0.9]
232
+ };
233
+
234
+ log('Sending request to API...');
235
+
236
+ // Llamar a la API
237
+ const response = await fetch(`${API_BASE_URL}/forecast_univariate`, {
238
+ method: 'POST',
239
+ headers: { 'Content-Type': 'application/json' },
240
+ body: JSON.stringify(requestBody)
241
+ });
242
+
243
+ if (!response.ok) {
244
+ throw new Error(`API error: ${response.statusText}`);
245
+ }
246
+
247
+ const data = await response.json();
248
+
249
+ log(`Received forecast for ${data.timestamps.length} periods`, 'success');
250
+
251
+ // Escribir resultados
252
+ await Excel.run(async (context) => {
253
+ const selection = context.workbook.getSelectedRange();
254
+ selection.load('rowIndex, rowCount');
255
+ await context.sync();
256
+
257
+ const startRow = selection.rowIndex + selection.rowCount + 2;
258
+
259
+ await writeForecastResults(
260
+ data.timestamps,
261
+ data.median,
262
+ data.quantiles['0.1'],
263
+ data.quantiles['0.9'],
264
+ startRow
265
+ );
266
+ });
267
+
268
+ log('✨ Forecast written to spreadsheet', 'success');
269
+
270
+ } catch (error) {
271
+ log(`Error: ${error.message}`, 'error');
272
+ console.error(error);
273
+ }
274
+ }
275
+
276
+ // ====================================================================
277
+ // FUNCIÓN 2: DETECCIÓN DE ANOMALÍAS
278
+ // ====================================================================
279
+
280
+ async function detectAnomalies() {
281
+ log('Starting anomaly detection...');
282
+
283
+ try {
284
+ const selection = await getSelectedRange();
285
+ const values = selection.values.flat().filter(v => v !== '' && !isNaN(v));
286
+
287
+ const contextLength = parseInt(document.getElementById('contextLength').value);
288
+ const recentPoints = parseInt(document.getElementById('recentPoints').value);
289
+
290
+ if (values.length < contextLength + recentPoints) {
291
+ log(`Error: Need at least ${contextLength + recentPoints} points`, 'error');
292
+ return;
293
+ }
294
+
295
+ const context = values.slice(0, contextLength);
296
+ const recent = values.slice(contextLength, contextLength + recentPoints);
297
+
298
+ const requestBody = {
299
+ context: { values: context },
300
+ recent_observed: recent,
301
+ prediction_length: recentPoints,
302
+ quantile_low: 0.05,
303
+ quantile_high: 0.95
304
+ };
305
+
306
+ log('Analyzing data...');
307
+
308
+ const response = await fetch(`${API_BASE_URL}/detect_anomalies`, {
309
+ method: 'POST',
310
+ headers: { 'Content-Type': 'application/json' },
311
+ body: JSON.stringify(requestBody)
312
+ });
313
+
314
+ if (!response.ok) {
315
+ throw new Error(`API error: ${response.statusText}`);
316
+ }
317
+
318
+ const data = await response.json();
319
+ const anomalyCount = data.anomalies.filter(a => a.is_anomaly).length;
320
+
321
+ if (anomalyCount > 0) {
322
+ log(`⚠️ Found ${anomalyCount} anomalies!`, 'error');
323
+ data.anomalies.filter(a => a.is_anomaly).forEach(a => {
324
+ log(` Point ${a.index}: value=${a.value.toFixed(2)}, expected=${a.predicted_median.toFixed(2)}`);
325
+ });
326
+ } else {
327
+ log('No anomalies detected ✓', 'success');
328
+ }
329
+
330
+ } catch (error) {
331
+ log(`Error: ${error.message}`, 'error');
332
+ console.error(error);
333
+ }
334
+ }
335
+
336
+ // ====================================================================
337
+ // FUNCIÓN 3: BACKTEST
338
+ // ====================================================================
339
+
340
+ async function runBacktest() {
341
+ log('Running backtest...');
342
+
343
+ try {
344
+ const selection = await getSelectedRange();
345
+ const values = selection.values.flat().filter(v => v !== '' && !isNaN(v));
346
+
347
+ const testLength = parseInt(document.getElementById('testLength').value);
348
+
349
+ if (values.length <= testLength) {
350
+ log('Error: Series must be longer than test length', 'error');
351
+ return;
352
+ }
353
+
354
+ const requestBody = {
355
+ series: { values: values },
356
+ prediction_length: testLength,
357
+ test_length: testLength
358
+ };
359
+
360
+ log('Evaluating model...');
361
+
362
+ const response = await fetch(`${API_BASE_URL}/backtest_simple`, {
363
+ method: 'POST',
364
+ headers: { 'Content-Type': 'application/json' },
365
+ body: JSON.stringify(requestBody)
366
+ });
367
+
368
+ if (!response.ok) {
369
+ throw new Error(`API error: ${response.statusText}`);
370
+ }
371
+
372
+ const data = await response.json();
373
+ const metrics = data.metrics;
374
+
375
+ log(`📊 Backtest Results:`, 'success');
376
+ log(` MAE: ${metrics.mae.toFixed(2)}`);
377
+ log(` MAPE: ${metrics.mape.toFixed(2)}%`);
378
+ log(` WQL: ${metrics.wql.toFixed(3)}`);
379
+
380
+ // Interpretar resultados
381
+ if (metrics.mae < 5) {
382
+ log(' Quality: Excellent ⭐⭐⭐⭐⭐', 'success');
383
+ } else if (metrics.mae < 10) {
384
+ log(' Quality: Good ⭐⭐⭐⭐');
385
+ } else {
386
+ log(' Quality: Moderate ⭐⭐⭐');
387
+ }
388
+
389
+ } catch (error) {
390
+ log(`Error: ${error.message}`, 'error');
391
+ console.error(error);
392
+ }
393
+ }
394
+
395
+ // ====================================================================
396
+ // FUNCIÓN 4: MULTI-SERIES
397
+ // ====================================================================
398
+
399
+ async function forecastMultiSeries() {
400
+ log('Starting multi-series forecast...');
401
+
402
+ try {
403
+ const selection = await getSelectedRange();
404
+ const data = selection.values;
405
+
406
+ // Agrupar por series_id (columna A)
407
+ const seriesMap = {};
408
+
409
+ for (let i = 1; i < data.length; i++) { // Skip header
410
+ const seriesId = data[i][0];
411
+ const value = data[i][2]; // Columna C
412
+
413
+ if (seriesId && value !== '' && !isNaN(value)) {
414
+ if (!seriesMap[seriesId]) {
415
+ seriesMap[seriesId] = [];
416
+ }
417
+ seriesMap[seriesId].push(parseFloat(value));
418
+ }
419
+ }
420
+
421
+ const seriesList = Object.entries(seriesMap).map(([id, values]) => ({
422
+ series_id: id,
423
+ values: values
424
+ }));
425
+
426
+ if (seriesList.length === 0) {
427
+ log('Error: No valid series found', 'error');
428
+ return;
429
+ }
430
+
431
+ log(`Found ${seriesList.length} series`);
432
+
433
+ const predictionLength = parseInt(document.getElementById('multiPredLength').value);
434
+
435
+ const requestBody = {
436
+ prediction_length: predictionLength,
437
+ series_list: seriesList,
438
+ start_timestamp: new Date().toISOString().split('T')[0],
439
+ freq: 'D',
440
+ quantile_levels: [0.1, 0.5, 0.9]
441
+ };
442
+
443
+ log('Forecasting all series...');
444
+
445
+ const response = await fetch(`${API_BASE_URL}/forecast_multi_id`, {
446
+ method: 'POST',
447
+ headers: { 'Content-Type': 'application/json' },
448
+ body: JSON.stringify(requestBody)
449
+ });
450
+
451
+ if (!response.ok) {
452
+ throw new Error(`API error: ${response.statusText}`);
453
+ }
454
+
455
+ const result = await response.json();
456
+
457
+ log(`✨ Generated forecasts for ${result.forecasts.length} series`, 'success');
458
+
459
+ // Escribir resultados (simplificado - solo mostrar en log)
460
+ result.forecasts.forEach(forecast => {
461
+ log(` ${forecast.series_id}: ${forecast.median.length} periods`);
462
+ });
463
+
464
+ } catch (error) {
465
+ log(`Error: ${error.message}`, 'error');
466
+ console.error(error);
467
+ }
468
+ }
469
+
470
+ // ====================================================================
471
+ // FUNCIÓN 5: COVARIABLES
472
+ // ====================================================================
473
+
474
+ async function forecastWithCovariates() {
475
+ log('Starting forecast with covariates...');
476
+
477
+ try {
478
+ const selection = await getSelectedRange();
479
+ const data = selection.values;
480
+
481
+ if (data.length < 3) {
482
+ log('Error: Need at least 3 rows of data', 'error');
483
+ return;
484
+ }
485
+
486
+ // Obtener parámetros
487
+ const predictionLength = parseInt(document.getElementById('covPredLength').value);
488
+ const covariateNamesInput = document.getElementById('covariateNames').value;
489
+ const covariateNames = covariateNamesInput.split(',').map(s => s.trim());
490
+
491
+ log(`Reading data with ${covariateNames.length} covariates: ${covariateNames.join(', ')}`);
492
+
493
+ // Estructura esperada:
494
+ // Col A: Date/Timestamp
495
+ // Col B: Target value
496
+ // Col C+: Covariates
497
+
498
+ const context = [];
499
+ const future = [];
500
+
501
+ for (let i = 1; i < data.length; i++) { // Skip header
502
+ const timestamp = data[i][0] ? data[i][0].toString() : null;
503
+ const target = data[i][1];
504
+
505
+ // Leer covariables
506
+ const covariates = {};
507
+ for (let j = 0; j < covariateNames.length && j < data[i].length - 2; j++) {
508
+ const covValue = data[i][j + 2];
509
+ if (covValue !== '' && !isNaN(covValue)) {
510
+ covariates[covariateNames[j]] = parseFloat(covValue);
511
+ }
512
+ }
513
+
514
+ // Si tiene target, es contexto histórico
515
+ if (target !== '' && !isNaN(target)) {
516
+ context.push({
517
+ timestamp: timestamp,
518
+ target: parseFloat(target),
519
+ covariates: covariates
520
+ });
521
+ }
522
+ // Si no tiene target pero sí covariables, son valores futuros
523
+ else if (Object.keys(covariates).length > 0) {
524
+ future.push({
525
+ timestamp: timestamp,
526
+ covariates: covariates
527
+ });
528
+ }
529
+ }
530
+
531
+ if (context.length === 0) {
532
+ log('Error: No historical data found', 'error');
533
+ return;
534
+ }
535
+
536
+ log(`Context: ${context.length} points, Future: ${future.length} points`);
537
+
538
+ const requestBody = {
539
+ context: context,
540
+ future: future.length > 0 ? future : null,
541
+ prediction_length: predictionLength,
542
+ quantile_levels: [0.1, 0.5, 0.9]
543
+ };
544
+
545
+ log('Calling API with covariates...');
546
+
547
+ const response = await fetch(`${API_BASE_URL}/forecast_with_covariates`, {
548
+ method: 'POST',
549
+ headers: { 'Content-Type': 'application/json' },
550
+ body: JSON.stringify(requestBody)
551
+ });
552
+
553
+ if (!response.ok) {
554
+ const errorText = await response.text();
555
+ throw new Error(`API error: ${response.statusText} - ${errorText}`);
556
+ }
557
+
558
+ const result = await response.json();
559
+
560
+ log(`✨ Forecast generated with ${result.pred_df.length} predictions`, 'success');
561
+
562
+ // Escribir resultados en una nueva ubicación
563
+ await Excel.run(async (context) => {
564
+ const selection = context.workbook.getSelectedRange();
565
+ selection.load('rowIndex, rowCount, columnCount');
566
+ await context.sync();
567
+
568
+ const startRow = selection.rowIndex + selection.rowCount + 2;
569
+ const startCol = 0;
570
+
571
+ // Crear tabla con los resultados
572
+ const sheet = context.workbook.worksheets.getActiveWorksheet();
573
+
574
+ // Headers
575
+ const headers = Object.keys(result.pred_df[0]);
576
+ const tableData = [headers];
577
+
578
+ // Data rows
579
+ result.pred_df.forEach(row => {
580
+ const rowData = headers.map(h => row[h]);
581
+ tableData.push(rowData);
582
+ });
583
+
584
+ const outputRange = sheet.getRangeByIndexes(
585
+ startRow,
586
+ startCol,
587
+ tableData.length,
588
+ headers.length
589
+ );
590
+
591
+ outputRange.values = tableData;
592
+ outputRange.format.autofitColumns();
593
+
594
+ // Format header
595
+ const headerRange = sheet.getRangeByIndexes(startRow, startCol, 1, headers.length);
596
+ headerRange.format.font.bold = true;
597
+ headerRange.format.fill.color = '#4472C4';
598
+ headerRange.format.font.color = 'white';
599
+
600
+ await context.sync();
601
+ });
602
+
603
+ log('✨ Results written to spreadsheet', 'success');
604
+
605
+ } catch (error) {
606
+ log(`Error: ${error.message}`, 'error');
607
+ console.error(error);
608
+ }
609
+ }
610
+
611
+ // ====================================================================
612
+ // FUNCIÓN 6: ESCENARIOS
613
+ // ====================================================================
614
+
615
+ async function generateScenarios() {
616
+ log('Starting scenario generation...');
617
+
618
+ try {
619
+ const selection = await getSelectedRange();
620
+ const data = selection.values;
621
+
622
+ if (data.length < 3) {
623
+ log('Error: Need at least 3 rows of data', 'error');
624
+ return;
625
+ }
626
+
627
+ const numScenarios = parseInt(document.getElementById('numScenarios').value);
628
+
629
+ // Estructura esperada similar a covariates:
630
+ // Col A: Date, Col B: Target, Col C+: Covariates
631
+ // Para escenarios, generaremos variaciones de las covariables
632
+
633
+ const context = [];
634
+ const covariateNames = [];
635
+
636
+ // Detectar nombres de covariables del header
637
+ for (let j = 2; j < data[0].length; j++) {
638
+ if (data[0][j]) {
639
+ covariateNames.push(data[0][j].toString());
640
+ }
641
+ }
642
+
643
+ log(`Detected covariates: ${covariateNames.join(', ')}`);
644
+
645
+ // Leer contexto histórico
646
+ for (let i = 1; i < data.length; i++) {
647
+ const timestamp = data[i][0] ? data[i][0].toString() : null;
648
+ const target = data[i][1];
649
+
650
+ if (target !== '' && !isNaN(target)) {
651
+ const covariates = {};
652
+ for (let j = 0; j < covariateNames.length && j < data[i].length - 2; j++) {
653
+ const covValue = data[i][j + 2];
654
+ if (covValue !== '' && !isNaN(covValue)) {
655
+ covariates[covariateNames[j]] = parseFloat(covValue);
656
+ }
657
+ }
658
+
659
+ context.push({
660
+ timestamp: timestamp,
661
+ target: parseFloat(target),
662
+ covariates: covariates
663
+ });
664
+ }
665
+ }
666
+
667
+ if (context.length === 0) {
668
+ log('Error: No historical data found', 'error');
669
+ return;
670
+ }
671
+
672
+ // Generar escenarios automáticamente
673
+ const predictionLength = 7;
674
+ const scenarios = [];
675
+
676
+ // Calcular valores promedio de covariables para generar variaciones
677
+ const avgCovariates = {};
678
+ covariateNames.forEach(name => {
679
+ const values = context
680
+ .map(p => p.covariates[name])
681
+ .filter(v => v !== undefined);
682
+ avgCovariates[name] = values.length > 0
683
+ ? values.reduce((a, b) => a + b, 0) / values.length
684
+ : 0;
685
+ });
686
+
687
+ // Escenario 1: Baseline (promedios)
688
+ const baselineScenario = {
689
+ name: 'Baseline',
690
+ future_covariates: []
691
+ };
692
+
693
+ for (let i = 0; i < predictionLength; i++) {
694
+ baselineScenario.future_covariates.push({
695
+ timestamp: `future_${i+1}`,
696
+ covariates: {...avgCovariates}
697
+ });
698
+ }
699
+ scenarios.push(baselineScenario);
700
+
701
+ // Escenario 2: Optimista (+20%)
702
+ if (numScenarios >= 2) {
703
+ const optimisticScenario = {
704
+ name: 'Optimistic (+20%)',
705
+ future_covariates: []
706
+ };
707
+
708
+ for (let i = 0; i < predictionLength; i++) {
709
+ const covs = {};
710
+ covariateNames.forEach(name => {
711
+ covs[name] = avgCovariates[name] * 1.2;
712
+ });
713
+ optimisticScenario.future_covariates.push({
714
+ timestamp: `future_${i+1}`,
715
+ covariates: covs
716
+ });
717
+ }
718
+ scenarios.push(optimisticScenario);
719
+ }
720
+
721
+ // Escenario 3: Pesimista (-20%)
722
+ if (numScenarios >= 3) {
723
+ const pessimisticScenario = {
724
+ name: 'Pessimistic (-20%)',
725
+ future_covariates: []
726
+ };
727
+
728
+ for (let i = 0; i < predictionLength; i++) {
729
+ const covs = {};
730
+ covariateNames.forEach(name => {
731
+ covs[name] = avgCovariates[name] * 0.8;
732
+ });
733
+ pessimisticScenario.future_covariates.push({
734
+ timestamp: `future_${i+1}`,
735
+ covariates: covs
736
+ });
737
+ }
738
+ scenarios.push(pessimisticScenario);
739
+ }
740
+
741
+ log(`Generated ${scenarios.length} scenarios`);
742
+
743
+ const requestBody = {
744
+ context: context,
745
+ scenarios: scenarios,
746
+ prediction_length: predictionLength,
747
+ quantile_levels: [0.1, 0.5, 0.9]
748
+ };
749
+
750
+ log('Calling scenarios API...');
751
+
752
+ const response = await fetch(`${API_BASE_URL}/forecast_scenarios`, {
753
+ method: 'POST',
754
+ headers: { 'Content-Type': 'application/json' },
755
+ body: JSON.stringify(requestBody)
756
+ });
757
+
758
+ if (!response.ok) {
759
+ const errorText = await response.text();
760
+ throw new Error(`API error: ${response.statusText} - ${errorText}`);
761
+ }
762
+
763
+ const result = await response.json();
764
+
765
+ log(`✨ Generated ${result.scenarios.length} scenario forecasts`, 'success');
766
+
767
+ // Escribir resultados
768
+ await Excel.run(async (context) => {
769
+ const selection = context.workbook.getSelectedRange();
770
+ selection.load('rowIndex, rowCount');
771
+ await context.sync();
772
+
773
+ const startRow = selection.rowIndex + selection.rowCount + 2;
774
+ const sheet = context.workbook.worksheets.getActiveWorksheet();
775
+
776
+ let currentRow = startRow;
777
+
778
+ // Escribir cada escenario
779
+ result.scenarios.forEach(scenario => {
780
+ // Header del escenario
781
+ const scenarioHeaderRange = sheet.getRangeByIndexes(currentRow, 0, 1, 1);
782
+ scenarioHeaderRange.values = [[`Scenario: ${scenario.name}`]];
783
+ scenarioHeaderRange.format.font.bold = true;
784
+ scenarioHeaderRange.format.fill.color = '#70AD47';
785
+ scenarioHeaderRange.format.font.color = 'white';
786
+ currentRow++;
787
+
788
+ // Datos del escenario
789
+ if (scenario.pred_df && scenario.pred_df.length > 0) {
790
+ const headers = Object.keys(scenario.pred_df[0]);
791
+ const tableData = [headers];
792
+
793
+ scenario.pred_df.forEach(row => {
794
+ tableData.push(headers.map(h => row[h]));
795
+ });
796
+
797
+ const dataRange = sheet.getRangeByIndexes(
798
+ currentRow,
799
+ 0,
800
+ tableData.length,
801
+ headers.length
802
+ );
803
+ dataRange.values = tableData;
804
+ dataRange.format.autofitColumns();
805
+
806
+ currentRow += tableData.length + 1; // +1 para separación
807
+ }
808
+ });
809
+
810
+ await context.sync();
811
+ });
812
+
813
+ log('✨ Scenarios written to spreadsheet', 'success');
814
+
815
+ } catch (error) {
816
+ log(`Error: ${error.message}`, 'error');
817
+ console.error(error);
818
+ }
819
+ }
820
+
821
+ // ====================================================================
822
+ // FUNCIÓN 7: MULTIVARIANTE
823
+ // ====================================================================
824
+
825
+ async function forecastMultivariate() {
826
+ log('Starting multivariate forecast...');
827
+
828
+ try {
829
+ const selection = await getSelectedRange();
830
+ const data = selection.values;
831
+
832
+ if (data.length < 3) {
833
+ log('Error: Need at least 3 rows of data', 'error');
834
+ return;
835
+ }
836
+
837
+ // Obtener parámetros
838
+ const predictionLength = parseInt(document.getElementById('multivarPredLength').value);
839
+ const targetColumnsInput = document.getElementById('targetColumns').value;
840
+ const targetColumns = targetColumnsInput.split(',').map(s => s.trim());
841
+
842
+ log(`Forecasting ${targetColumns.length} target variables: ${targetColumns.join(', ')}`);
843
+
844
+ // Estructura esperada:
845
+ // Col A: Date/Timestamp
846
+ // Col B+: Target variables (múltiples columnas que queremos predecir)
847
+
848
+ const context = [];
849
+
850
+ // Validar que hay suficientes columnas
851
+ if (data[0].length < targetColumns.length + 1) {
852
+ log(`Error: Expected ${targetColumns.length + 1} columns but found ${data[0].length}`, 'error');
853
+ return;
854
+ }
855
+
856
+ // Leer datos
857
+ for (let i = 1; i < data.length; i++) { // Skip header
858
+ const timestamp = data[i][0] ? data[i][0].toString() : null;
859
+
860
+ // Leer todos los targets
861
+ const targets = {};
862
+ let hasValidData = false;
863
+
864
+ for (let j = 0; j < targetColumns.length && j < data[i].length - 1; j++) {
865
+ const value = data[i][j + 1];
866
+ if (value !== '' && !isNaN(value)) {
867
+ targets[targetColumns[j]] = parseFloat(value);
868
+ hasValidData = true;
869
+ }
870
+ }
871
+
872
+ if (hasValidData) {
873
+ context.push({
874
+ timestamp: timestamp,
875
+ targets: targets,
876
+ covariates: {} // Sin covariables por ahora
877
+ });
878
+ }
879
+ }
880
+
881
+ if (context.length === 0) {
882
+ log('Error: No valid data found', 'error');
883
+ return;
884
+ }
885
+
886
+ log(`Read ${context.length} data points`);
887
+
888
+ const requestBody = {
889
+ context: context,
890
+ target_columns: targetColumns,
891
+ prediction_length: predictionLength,
892
+ quantile_levels: [0.1, 0.5, 0.9]
893
+ };
894
+
895
+ log('Calling multivariate forecast API...');
896
+
897
+ const response = await fetch(`${API_BASE_URL}/forecast_multivariate`, {
898
+ method: 'POST',
899
+ headers: { 'Content-Type': 'application/json' },
900
+ body: JSON.stringify(requestBody)
901
+ });
902
+
903
+ if (!response.ok) {
904
+ const errorText = await response.text();
905
+ throw new Error(`API error: ${response.statusText} - ${errorText}`);
906
+ }
907
+
908
+ const result = await response.json();
909
+
910
+ log(`✨ Generated multivariate forecast with ${result.pred_df.length} predictions`, 'success');
911
+
912
+ // Escribir resultados
913
+ await Excel.run(async (context) => {
914
+ const selection = context.workbook.getSelectedRange();
915
+ selection.load('rowIndex, rowCount');
916
+ await context.sync();
917
+
918
+ const startRow = selection.rowIndex + selection.rowCount + 2;
919
+ const sheet = context.workbook.worksheets.getActiveWorksheet();
920
+
921
+ // Crear tabla con resultados
922
+ if (result.pred_df && result.pred_df.length > 0) {
923
+ const headers = Object.keys(result.pred_df[0]);
924
+ const tableData = [headers];
925
+
926
+ result.pred_df.forEach(row => {
927
+ tableData.push(headers.map(h => row[h]));
928
+ });
929
+
930
+ const outputRange = sheet.getRangeByIndexes(
931
+ startRow,
932
+ 0,
933
+ tableData.length,
934
+ headers.length
935
+ );
936
+
937
+ outputRange.values = tableData;
938
+ outputRange.format.autofitColumns();
939
+
940
+ // Format header
941
+ const headerRange = sheet.getRangeByIndexes(startRow, 0, 1, headers.length);
942
+ headerRange.format.font.bold = true;
943
+ headerRange.format.fill.color = '#4472C4';
944
+ headerRange.format.font.color = 'white';
945
+
946
+ await context.sync();
947
+ }
948
+ });
949
+
950
+ log('✨ Multivariate forecast written to spreadsheet', 'success');
951
+
952
+ } catch (error) {
953
+ log(`Error: ${error.message}`, 'error');
954
+ console.error(error);
955
+ }
956
+ }