Spaces:
Build error
Build error
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 +3 -0
- app/main.py +79 -5
- static/assets/icon-16.png +1 -0
- static/assets/icon-32.png +1 -0
- static/assets/icon-64.png +1 -0
- static/assets/icon-80.png +1 -0
- static/commands/commands.html +15 -0
- static/manifest.xml +105 -0
- static/taskpane/taskpane.css +308 -0
- static/taskpane/taskpane.html +218 -0
- static/taskpane/taskpane.js +956 -0
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
|
| 24 |
-
"multivariante, covariables, escenarios, anomalías y backtesting."
|
|
|
|
| 25 |
),
|
| 26 |
-
version="1.0
|
| 27 |
)
|
| 28 |
|
| 29 |
# Configurar CORS para Excel Add-in
|
| 30 |
app.add_middleware(
|
| 31 |
CORSMiddleware,
|
| 32 |
-
allow_origins=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|